dobrograd-13-06-2022/garrysmod/addons/feature-wire/lua/wire/gpulib.lua
Jonny_Bro (Nikita) e4d5311906 first commit
2023-11-16 15:01:19 +05:00

868 lines
24 KiB
Lua

--------------------------------------------------------------------------------
-- WireGPU class
--------------------------------------------------------------------------------
-- Usage:
-- Initialize:
-- self.GPU = WireGPU(self.Entity)
--
-- OnRemove:
-- self.GPU:Finalize()
--
-- Draw (if something changes):
-- self.GPU:RenderToGPU(function()
-- ...code...
-- end)
--
-- Draw (every frame):
-- self.GPU:Render()
--------------------------------------------------------------------------------
GPULib = {}
local GPU = {}
GPU.__index = GPU
GPULib.GPU = GPU
function GPULib.WireGPU(ent, ...)
local self = {
entindex = ent and ent:EntIndex() or 0,
Entity = ent or NULL,
}
setmetatable(self, GPU)
self:Initialize(...)
return self
end
WireGPU = GPULib.WireGPU
function GPU:SetTranslucentOverride(bool)
self.translucent = bool;
end
function GPU:GetInfo()
local ent = self.Entity
if not ent:IsValid() then ent = self.actualEntity end
if not ent then return end
local model = ent:GetModel()
local monitor = WireGPU_Monitors[model]
local pos = ent:LocalToWorld(monitor.offset)
local ang = ent:LocalToWorldAngles(monitor.rot)
return monitor, pos, ang
end
if CLIENT then
local materialCache = {}
function GPULib.Material(name)
if not materialCache[name] then
local protoMaterial = Material(name)
local textureName = protoMaterial:GetString("$basetexture")
local imageName = protoMaterial:GetName()
local materialParameters = {
["$basetexture"] = textureName,
["$vertexcolor"] = 1,
["$vertexalpha"] = 1,
}
materialCache[name] = CreateMaterial(imageName.."_DImage", "UnlitGeneric", materialParameters)
end
return materialCache[name]
end
-- Handles rendertarget caching
local RT_CACHE_SIZE = 32
local RenderTargetCache = { }
for i = 1,RT_CACHE_SIZE do
local Target = {
false, -- Is rendertarget in use
false -- The rendertarget (false if doesn't exist)
}
table.insert( RenderTargetCache, Target )
end
-- Returns a render target from the cache pool and marks it as used
local function GetRT()
for i, RT in pairs( RenderTargetCache ) do
if not RT[1] then -- not used
local rendertarget = RT[2]
if rendertarget then
RT[1] = true -- Mark as used
return rendertarget
end
end
end
-- No free rendertargets. Find first non used and create it.
for i, RT in pairs( RenderTargetCache ) do
if not RT[1] and RT[2] == false then -- not used and doesn't exist, let's create the render target.
local rendertarget = GetRenderTarget("WireGPU_RT_"..i, 512, 512)
if rendertarget then
RT[1] = true -- Mark as used
RT[2] = rendertarget -- Assign the RT
return rendertarget
else
RT[1] = true -- Mark as used since we couldn't create it
ErrorNoHalt("Wiremod: Render target ".."WireGPU_RT_"..i.." could not be created!\n")
end
end
end
ErrorNoHalt("All render targets are in use, some wire screens may not draw!\n")
return nil
end
-- Frees an used RT
local function FreeRT(rt)
for i, RT in pairs( RenderTargetCache ) do
if RT[2] == rt then
RT[1] = false
return
end
end
ErrorNoHalt("RT Screen ",rt," could not be freed (not found)\n")
end
//
// Create basic fonts
//
local fontData =
{
font="lucida console",
size=20,
weight=800,
antialias= true,
additive = false,
}
surface.CreateFont("WireGPU_ConsoleFont", fontData)
//
// Create screen textures and materials
//
WireGPU_matScreen = CreateMaterial("sprites/GPURT","UnlitGeneric",{
["$vertexcolor"] = 1,
["$vertexalpha"] = 1,
["$translucent"] = 1,
["$ignorez"] = 1,
["$nolod"] = 1,
})
WireGPU_matBuffer = CreateMaterial("sprites/GPUBUF","UnlitGeneric",{
["$vertexcolor"] = 1,
["$vertexalpha"] = 1,
["$translucent"] = 1,
["$ignorez"] = 1,
["$nolod"] = 1,
})
function GPU:Initialize(no_rendertarget)
if no_rendertarget then return nil end
-- Rendertarget cache management
-- This should not even happen.
if self.RT then
ErrorNoHalt("Warning: GPU:Initialize called, but an RT still existed. Maybe you are not killing it properly?")
FreeRT(self.RT)
end
-- find a free one
self.RT = GetRT()
if not self.RT then
return nil
end
-- clear the new RT
self.ForceClear = true
return self.RT
end
function GPULib.WireGPU(ent, ...)
local self = {
entindex = ent and ent:EntIndex() or 0,
Entity = ent or NULL,
}
setmetatable(self, GPU)
self:Initialize(...)
return self
end
function GPU:Finalize()
if not self.RT then return end
timer.Simple(0.2, function() -- This is to test if the entity has truly been removed. If you really know you need to remove the RT, call FreeRT()
if IsValid(self.Entity) then
--MsgN(self,"Entity still exists, exiting.")
return
end
self:FreeRT()
end)
end
function GPU:FreeRT()
FreeRT( self.RT )
self.RT = nil
end
function GPU:Clear(color)
if not self.RT then return end
render.ClearRenderTarget(self.RT, color or Color(0, 0, 0, 0))
end
local texcoords = {
[0] = {
{ u = 0, v = 0 },
{ u = 1, v = 0 },
{ u = 1, v = 1 },
{ u = 0, v = 1 },
},
{
{ u = 0, v = 1 },
{ u = 0, v = 0 },
{ u = 1, v = 0 },
{ u = 1, v = 1 },
},
{
{ u = 1, v = 1 },
{ u = 0, v = 1 },
{ u = 0, v = 0 },
{ u = 1, v = 0 },
},
{
{ u = 1, v = 0 },
{ u = 1, v = 1 },
{ u = 0, v = 1 },
{ u = 0, v = 0 },
},
}
-- helper function for GPU:Render
function GPU.DrawScreen(x, y, w, h, rotation, scale)
-- generate vertex data
local vertices = {
--[[
Vector(x , y ),
Vector(x+w, y ),
Vector(x+w, y+h),
Vector(x , y+h),
]]
{ x = x , y = y },
{ x = x+w, y = y },
{ x = x+w, y = y+h },
{ x = x , y = y+h },
}
-- rotation and scaling
local rotated_texcoords = texcoords[rotation] or texcoords[0]
for index,vertex in ipairs(vertices) do
local tex = rotated_texcoords[index]
if tex.u == 0 then
vertex.u = tex.u-scale
else
vertex.u = tex.u+scale
end
if tex.v == 0 then
vertex.v = tex.v-scale
else
vertex.v = tex.v+scale
end
end
surface.DrawPoly(vertices)
--render.DrawQuad(unpack(vertices))
end
function GPU:RenderToGPU(renderfunction)
if not self.RT then return end
if self.ForceClear then
self:Clear()
self.ForceClear = nil
end
local oldw = ScrW()
local oldh = ScrH()
local NewRT = self.RT
local OldRT = render.GetRenderTarget()
render.SetRenderTarget(NewRT)
render.SetViewPort(0, 0, 512, 512)
cam.Start2D()
local ok, err = xpcall(renderfunction, debug.traceback)
if not ok then WireLib.ErrorNoHalt(err) end
cam.End2D()
render.SetViewPort(0, 0, oldw, oldh)
render.SetRenderTarget(OldRT)
end
-- If width is specified, height is ignored. if neither is specified, a height of 512 is used.
function GPU:RenderToWorld(width, height, renderfunction, zoffset, emulateRT)
local monitor, pos, ang = self:GetInfo()
if zoffset then
pos = pos + ang:Up()*zoffset
end
if emulateRT then
pos = pos - ang:Right()*(monitor.y2-monitor.y1)/2
pos = pos - ang:Forward()*(monitor.x2-monitor.x1)/2
end
local h = width and width*monitor.RatioX or height or 512
local w = width or h/monitor.RatioX
local x = -w/2
local y = -h/2
local res = monitor.RS*512/h
cam.Start3D2D(pos, ang, res)
local ok, err = xpcall(renderfunction, debug.traceback, x, y, w, h, monitor, pos, ang, res)
if not ok then WireLib.ErrorNoHalt(err) end
cam.End3D2D()
end
function GPU:Render(rotation, scale, width, height, postrenderfunction)
if not self.RT then return end
local monitor, pos, ang = self:GetInfo()
local OldTex = WireGPU_matScreen:GetTexture("$basetexture")
WireGPU_matScreen:SetTexture("$basetexture", self.RT)
local res = monitor.RS
cam.Start3D2D(pos, ang, res)
local ok, err = xpcall(function()
local aspect = 1/monitor.RatioX
local w = (width or 512)*aspect
local h = (height or 512)
local x = -w/2
local y = -h/2
local translucent = self.translucent;
if translucent == nil then
translucent = monitor.translucent
end
if not translucent then
surface.SetDrawColor(0,0,0,255)
surface.DrawRect(-256*aspect,-256,512*aspect,512)
end
surface.SetDrawColor(255,255,255,255)
surface.SetMaterial(WireGPU_matScreen)
render.PushFilterMag(self.texture_filtering or TEXFILTER.POINT)
render.PushFilterMin(self.texture_filtering or TEXFILTER.POINT)
self.DrawScreen(x, y, w, h, rotation or 0, scale or 0)
render.PopFilterMin()
render.PopFilterMag()
if postrenderfunction then postrenderfunction(pos, ang, res, aspect, monitor) end
end, debug.traceback)
if not ok then WireLib.ErrorNoHalt(err) end
cam.End3D2D()
WireGPU_matScreen:SetTexture("$basetexture", OldTex)
end
-- compatibility
local GPUs = {}
function WireGPU_NeedRenderTarget(entindex)
if not GPUs[entindex] then GPUs[entindex] = GPULib.WireGPU(Entity(entindex)) end
return GPUs[entindex].RT
end
function WireGPU_GetMyRenderTarget(entindex)
local self = GPUs[entindex]
if self.RT then return self.RT end
return self:Initialize()
end
function WireGPU_ReturnRenderTarget(entindex)
return GPUs[entindex]:Finalize()
end
function WireGPU_DrawScreen(x, y, w, h, rotation, scale)
return GPU.DrawScreen(x, y, w, h, rotation, scale)
end
end
-- GPULib switcher functionality
if CLIENT then
usermessage.Hook("wire_gpulib_setent", function(um)
local screen = Entity(um:ReadShort())
if not screen:IsValid() then return end
if not screen.GPU then return end
local ent = Entity(um:ReadShort())
if not ent:IsValid() then return end
screen.GPU.Entity = ent
screen.GPU.entindex = ent:EntIndex()
if screen == ent then return end
screen.GPU.actualEntity = screen
local model = ent:GetModel()
local monitor = WireGPU_Monitors[model]
local h = 512*monitor.RS
local w = h/monitor.RatioX
local x = -w/2
local y = -h/2
local corners = {
{ x , y },
{ x , y+h },
{ x+w, y },
{ x+w, y+h },
}
local mins, maxs = screen:OBBMins(), screen:OBBMaxs()
local timerid = "wire_gpulib_updatebounds"..screen:EntIndex()
local function setbounds()
if not screen:IsValid() then
timer.Remove(timerid)
return
end
if not ent:IsValid() then
timer.Remove(timerid)
screen.ExtraRBoxPoints[1001] = nil
screen.ExtraRBoxPoints[1002] = nil
screen.ExtraRBoxPoints[1003] = nil
screen.ExtraRBoxPoints[1004] = nil
Wire_UpdateRenderBounds(screen)
screen.GPU.Entity = screen.GPU.actualEntity
screen.GPU.entindex = screen.GPU.actualEntity:EntIndex()
screen.GPU.actualEntity = nil
return
end
local ang = ent:LocalToWorldAngles(monitor.rot)
local pos = ent:LocalToWorld(monitor.offset)
screen.ExtraRBoxPoints = screen.ExtraRBoxPoints or {}
for i,x,y in ipairs_map(corners, unpack) do
local p = Vector(x, y, 0)
p:Rotate(ang)
p = screen:WorldToLocal(p+pos)
screen.ExtraRBoxPoints[i+1000] = p
end
Wire_UpdateRenderBounds(screen)
end
timer.Create(timerid, 5, 0, setbounds)
setbounds()
end) -- usermessage.Hook
elseif SERVER then
function GPULib.switchscreen(screen, ent)
screen.GPUEntity = ent
umsg.Start("wire_gpulib_setent")
umsg.Short(screen:EntIndex())
umsg.Short(ent:EntIndex())
umsg.End()
end
end
-- GPULib caching functionality
if CLIENT then
------------------------------------------------------------------------------
-- Attach cache receiver to this entity
------------------------------------------------------------------------------
local writeHandler = {}
function GPULib.ClientCacheCallback(ent, writeFunction)
writeHandler[ent and ent:EntIndex() or 0] = writeFunction
end
------------------------------------------------------------------------------
-- RLE-decompress incoming message
------------------------------------------------------------------------------
--[[local blockText = {
{ "[no offset]", "[1-offset]", "[2-offset]", "[4-offset]" },
{ "[no rep]", nil, "[rep 2]", "[rep 4]" },
{ "[cnt 1]", nil, "[cnt 2]", "[cnt 3]" },
{ "[1-byte]", "[2-byte]", "[4-byte]", "[marker]" },
} ]]--
local function GPULib_MemorySync(um)
-- Find the referenced entity
local GPUIdx = um:ReadLong()
local GPU = ents.GetByIndex(GPUIdx)
if not GPU then return end
if not GPU:IsValid() then return end
if not writeHandler[GPUIdx] then return end
-- Start reading blocks
local blockCount = 0
local currentOffset = 0
while true do
-- Read next block
blockCount = blockCount + 1
if blockCount > 256 then error("GPULib usermessage read error") return end
-- Read block flags
local dataFlags = um:ReadChar()+128
if dataFlags == 240 then return end
local offsetSize = dataFlags % 4
local repeatCount = math.floor(dataFlags/4) % 4
local dataCount = math.floor(dataFlags/16) % 4
local valueSize = math.floor(dataFlags/64) % 4
local Repeat = 0
local Count = 0
if offsetSize > 0 then
local deltaOffset = 0
if offsetSize == 1 then deltaOffset = um:ReadChar () end
if offsetSize == 2 then deltaOffset = um:ReadShort() end
if offsetSize == 3 then deltaOffset = um:ReadFloat() end
currentOffset = currentOffset + deltaOffset
--print(" dOffset = "..deltaOffset..", offset = "..currentOffset)
end
if dataCount == 0 then Count = 1 end
if dataCount == 1 then Count = um:ReadChar()+130 end
if dataCount == 2 then Count = 2 end
if dataCount == 3 then Count = 3 end
if repeatCount == 0 then Repeat = 1 end
if repeatCount == 1 then Repeat = um:ReadChar()+130 end
if repeatCount == 2 then Repeat = 2 end
if repeatCount == 3 then Repeat = 4 end
--[[print(" Block ",
blockText[1][offsetSize+1],
blockText[2][repeatCount+1] or ("[rep "..Repeat.."* ]"),
blockText[3][dataCount+1] or ("[cnt "..Count.."* ]"),
blockText[4][valueSize+1])]]--
for i=1,Count do
local Value = 0
if valueSize == 0 then Value = um:ReadChar() end
if valueSize == 1 then Value = um:ReadShort() end
if valueSize == 2 then Value = um:ReadLong() end
if valueSize == 3 then Value = um:ReadFloat() end
for j=1,Repeat do
--print(" ["..currentOffset.."] = "..Value)
writeHandler[GPUIdx](currentOffset,Value)
currentOffset = currentOffset + 1
end
end
end
end
usermessage.Hook("wire_memsync", GPULib_MemorySync)
elseif SERVER then
local CACHEMGR = {}
CACHEMGR.__index = CACHEMGR
GPULib.CACHEMGR = CACHEMGR
------------------------------------------------------------------------------
-- Create new cache manager (serverside)
------------------------------------------------------------------------------
function GPULib.GPUCacheManager(ent, orderMatters, ...)
local self = {
EntIndex = ent and ent:EntIndex() or 0,
Entity = ent or NULL,
}
setmetatable(self, CACHEMGR)
self.ValueOrderMatters = orderMatters
self.Enabled = true
self:Reset()
return self
end
GPUCacheManager = GPULib.GPUCacheManager
------------------------------------------------------------------------------
-- Get size of the value to write
------------------------------------------------------------------------------
local function getSize(value)
if (value >= -128) and (value <= 127) and (math.floor(value) == value) then return 1,false end
if (value >= -32768) and (value <= 32767) and (math.floor(value) == value) then return 2,false end
if (value >= -2147483648) and (value <= 2147483647) and (math.floor(value) == value) then return 4,false end
return 4,true
end
------------------------------------------------------------------------------
-- Initialize cache manager
------------------------------------------------------------------------------
function CACHEMGR:Reset()
self.Cache = {}
self.CacheBytes = 0
end
------------------------------------------------------------------------------
-- Write a single value to cache
------------------------------------------------------------------------------
function CACHEMGR:Write(Address,Value)
local valueSize,valueFloat
if Value then
valueSize,valueFloat = getSize(Value)
self.CacheBytes = self.CacheBytes + valueSize
end
table.insert(self.Cache,{ Address, Value, valueSize, valueFloat })
--if #self.Cache > 2048 then self:Flush() end
end
------------------------------------------------------------------------------
-- Send value right away
------------------------------------------------------------------------------
function CACHEMGR:WriteNow(Address,Value,forcePlayer)
umsg.Start("wire_memsync", forcePlayer)
umsg.Long(self.EntIndex)
umsg.Char(195-128)
umsg.Float(Address)
umsg.Float(Value)
umsg.Char(240-128)
umsg.End()
end
------------------------------------------------------------------------------
-- RLE-compress cache and send it
------------------------------------------------------------------------------
function CACHEMGR:Flush(forcePlayer)
-- Don't flush if nothing cached
if #self.Cache == 0 then return end
self.CacheBytes = 0
-- Sort cache so all addresses are continiously layed out
-- Do not sort if order at which values are written matters
if not self.ValueOrderMatters then
table.sort(self.Cache,function(A,B)
return A[1] < B[1]
end)
end
-- RLE-encode the data
local compressInfo = {}
for _,data in ipairs(self.Cache) do
local address,value,size,isfloat = data[1],(data[2] or 0),(data[3] or 1),(data[4] or false)
local compressBlock = compressInfo[#compressInfo]
local sequentialBlock
local previousBlockEnd
if compressBlock then
previousBlockEnd = compressBlock.Offset+#compressBlock.Data*compressBlock.Repeat
sequentialBlock = previousBlockEnd == address
end
if not compressBlock then
-- New block of data
compressBlock = {
Data = { value },
Offset = address,
SetOffset = address,
Repeat = 1,
Size = size,
IsFloat = isfloat,
}
table.insert(compressInfo,compressBlock)
elseif sequentialBlock and
(compressBlock.Size == size) then
-- Add to previous block of data
if (#compressBlock.Data == 1) and (compressBlock.Data[1] == value) and (sequentialBlock) and (compressBlock.Repeat < 256) then
-- RLE compression
compressBlock.Repeat = compressBlock.Repeat + 1
elseif compressBlock.Repeat > 1 then
-- Cant add to a repeating block, make new
compressBlock = {
Data = { value },
Offset = address,
Repeat = 1,
Size = size,
IsFloat = isfloat,
}
if not sequentialBlock then compressBlock.SetOffset = address-previousBlockEnd end
table.insert(compressInfo,compressBlock)
else
-- Append to a group of values, unless the block is too big
if #compressBlock.Data*compressBlock.Repeat*compressBlock.Size < 196 then
table.insert(compressBlock.Data,value)
else
-- Add it to a new block instead
compressBlock = {
Data = { value },
Offset = address,
Repeat = 1,
Size = size,
IsFloat = isfloat,
}
if not sequentialBlock then compressBlock.SetOffset = address-previousBlockEnd end
table.insert(compressInfo,compressBlock)
end
end
else
-- Create new block
compressBlock = {
Data = { value },
Offset = address,
Repeat = 1,
Size = size,
IsFloat = isfloat,
}
if not sequentialBlock then compressBlock.SetOffset = address-previousBlockEnd end
table.insert(compressInfo,compressBlock)
end
end
--PrintTable(compressInfo)
-- Start the message
local messageSize = 4
umsg.Start("wire_memsync", forcePlayer)
umsg.Long(self.EntIndex)
-- Start sending all compressed blocks
for k,v in ipairs(compressInfo) do
--======================================================================--
-- Generate flags for sending the data
--======================================================================--
-- [0..1] Delta offset
-- 0: no offset
-- 1: 1-byte offset
-- 2: 2-byte offset
-- 3: 4-byte offset
-- [2..3] Repeat count
-- 0: none
-- 1: repeat count 1-byte follows
-- 2: repeat 2 times
-- 3: repeat 4 times
-- [4..5] Data count
-- 0: 1 element
-- 1: data size 1-byte follows
-- 2: 2 elements
-- 3: 3 elements (but not floats)
-- [6..7] Size
-- 0: 1-byte
-- 1: 2-byte
-- 2: 4-byte int
-- 3: 4-byte float
--
-- If it's a special data marker, then bitmap is:
-- [0..1] Marker type
-- [2..5] Marker data
-- [6] 1
-- [7] 1
local dataFlags = 0
if v.SetOffset then
local offsetSize = getSize(v.SetOffset)
if offsetSize == 1 then dataFlags = dataFlags + 1 end
if offsetSize == 2 then dataFlags = dataFlags + 2 end
if offsetSize == 4 then dataFlags = dataFlags + 3 end
end
if v.Repeat > 1 then
if v.Repeat == 2 then dataFlags = dataFlags + 8
elseif v.Repeat == 4 then dataFlags = dataFlags + 12
else dataFlags = dataFlags + 4
end
end
if #v.Data > 1 then
if #v.Data == 2 then
dataFlags = dataFlags + 32
elseif (#v.Data == 3) and (not v.IsFloat) then
dataFlags = dataFlags + 48
else
dataFlags = dataFlags + 16
end
end
if v.Size == 1 then dataFlags = dataFlags + 0 end
if v.Size == 2 then dataFlags = dataFlags + 64 end
if (v.Size == 4) and (not v.IsFloat) then dataFlags = dataFlags + 128 end
if (v.Size == 4) and ( v.IsFloat) then dataFlags = dataFlags + 192 end
umsg.Char(dataFlags-128)
messageSize = messageSize + 4
--======================================================================--
-- Send the data
--======================================================================--
if v.SetOffset then
local offsetSize = getSize(v.SetOffset)
if offsetSize == 1 then umsg.Char (v.SetOffset) messageSize = messageSize + 1 end
if offsetSize == 2 then umsg.Short(v.SetOffset) messageSize = messageSize + 2 end
if offsetSize == 4 then umsg.Float(v.SetOffset) messageSize = messageSize + 4 end
end
if (#v.Data > 2) then
if (#v.Data ~= 3) or (v.IsFloat) then
umsg.Char(#v.Data-130) messageSize = messageSize + 1
end
end
if (v.Repeat > 1) and
(v.Repeat ~= 2) and
(v.Repeat ~= 4) then umsg.Char(v.Repeat-130) messageSize = messageSize + 1 end
for _,value in ipairs(v.Data) do
if v.Size == 1 then umsg.Char (value) messageSize = messageSize + 1 end
if v.Size == 2 then umsg.Short(value) messageSize = messageSize + 2 end
if (v.Size == 4) and (not v.IsFloat) then umsg.Long(value) messageSize = messageSize + 4 end
if (v.Size == 4) and ( v.IsFloat) then umsg.Float(value) messageSize = messageSize + 4 end
end
--======================================================================--
-- Check size of next data block. If it fits into usermessage, continue.
-- Otherwise just create new message
--======================================================================--
if compressInfo[k+1] then
local nextSize = #compressInfo[k+1].Data*compressInfo[k+1].Repeat*compressInfo[k+1].Size
if nextSize + messageSize > 248 then
umsg.Char(240-128)
umsg.End()
messageSize = 4
umsg.Start("wire_memsync", forcePlayer)
umsg.Long(self.EntIndex)
compressInfo[k+1].SetOffset = compressInfo[k+1].Offset -- Force set offset
end
else
umsg.Char(240-128)
umsg.End()
end
end
self.Cache = {}
end
end