Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MCOPY is now limited by frequency + Interruptible instruction framework #38

Merged
merged 5 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions lua/entities/gmod_wire_cpu.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ function ENT:Initialize()
-- Create virtual machine
self.VM = CPULib.VirtualMachine()
self.VM.SerialNo = CPULib.GenerateSN("CPU")
-- Since the device supports (quota)interruptible instructions
-- Memory access can be a lot cheaper.
self.VM.MemoryReadCycles = 2
self.VM.MemoryWriteCycles = 2
self.VM.ExternalReadCycles = 8
self.VM.ExternalWriteCycles = 8
self.VM:Reset()

self:SetCPUName()
Expand Down Expand Up @@ -182,8 +188,18 @@ function ENT:Run()

while (Cycles > 0) and (self.Clk) and (not self.VMStopped) and (self.VM.Idle == 0) do
-- Run VM step
self.VM.QuotaSupported = true
local previousTMR = self.VM.TMR
self.VM:Step()
self.VM.Quota = self.VM.TMR + Cycles
if self.VM.QuotaOverrunFunc then
self.VM:QuotaOverrunFunc()
else
self.VM:Step()
end
if self.VM.EndedOnQuota then
self.VM.EndedOnQuota = false
end
self.VM.QuotaSupported = false
Cycles = Cycles - math.max(1, self.VM.TMR - previousTMR)
end
end
Expand Down Expand Up @@ -269,7 +285,9 @@ function ENT:TriggerInput(iname, value)
self:NextThink(CurTime())
end
elseif iname == "Frequency" then
if value > 0 then self.Frequency = math.floor(value) end
if value > 0 then
self.Frequency = math.floor(value)
end
elseif iname == "Reset" then --VM may be nil
if self.VM.HWDEBUG ~= 0 then
self.VM.DBGSTATE = math.floor(value)
Expand Down
1 change: 1 addition & 0 deletions lua/entities/gmod_wire_gpu/cl_gpuvm.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function ENT:OverrideVM()
self.VM.ErrorText[15] = "Address space violation"
self.VM.ErrorText[16] = "Pants integrity violation"
self.VM.ErrorText[17] = "Frame instruction limit"
self.VM.ErrorText[18] = "Frame delayed by quota overrun"
self.VM.ErrorText[23] = "Error reading string data"

self.VM.Interrupt = function(self,interruptNo,interruptParameter,isExternal,cascadeInterrupt)
Expand Down
46 changes: 40 additions & 6 deletions lua/entities/gmod_wire_gpu/cl_init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ function ENT:Initialize()
self.VM.VertexMode = 0
self.VM.MemBusBuffer = {}
self.VM.MemBusCount = 0
self.VM.LateFrames = 0

-- Create GPU
self.GPU = WireGPU(self)
Expand Down Expand Up @@ -136,30 +137,63 @@ function ENT:Run(isAsync)
Cycles = math.max(1,math.floor(self.VM.Memory[65527]*self.DeltaTime*0.5))
self.VM.TimerDT = self.DeltaTime/Cycles
self.VM.TIMER = self.CurrentTime
self.VM.QuotaOverrunFunc = self.VM.AsyncQuotaOverrun
self.VM.ASYNC = 1
else
Cycles = 50000
self.VM:Reset()
self.VM.TimerDT = self.DeltaTime
self.VM.TIMER = self.CurrentTime

if self.VM.INIT == 0 then
self.VM.IP = self.VM.EntryPoint1
self.VM.INIT = 1
else
self.VM.IP = self.VM.EntryPoint0
if self.VM.SyncQuotaIP then
self.VM.IP = self.VM.SyncQuotaIP
else
self.VM.IP = self.VM.EntryPoint0
end
end

self.VM.QuotaOverrunFunc = self.VM.SyncQuotaOverrun
self.VM.ASYNC = 0
end

local function getCode(self)
local mem = {}
for i=0,16 do
mem[i] = self.VM:ReadCell(i)
end
return mem
end
-- Run until interrupt, or if async thread then until async thread stops existing
while (Cycles > 0) and (self.VM.INTR == 0) do -- and (not (isAsync and (self.VM.Entrypoint4 == 0)))
self.VM.QuotaSupported = 1
self.VM.Quota = self.VM.TMR + Cycles
local previousTMR = self.VM.TMR
self.VM:Step()
if self.VM.QuotaOverrunFunc then
self.VM:QuotaOverrunFunc()
else
self.VM:Step()
end
Cycles = Cycles - (self.VM.TMR - previousTMR)

if (self.VM.ASYNC == 0) and (Cycles < 0) then self.VM:Interrupt(17,0) end
if (self.VM.ASYNC == 0) and (Cycles < 0) and not self.VM.QuotaOverrunFunc then self.VM:Interrupt(17,0) end
if (self.VM.ASYNC == 0) and (self.VM.LateFrames > 1) then self.VM:Interrupt(18,0) end
self.VM.QuotaSupported = 0
end

-- Will also handle cleanup of quota overrun, since VM.QuotaOverrun sets itself to nil if the func is done
if isAsync then
self.VM.AsyncQuotaOverrun = self.VM.QuotaOverrunFunc
else
self.VM.SyncQuotaOverrun = self.VM.QuotaOverrunFunc
if self.VM.SyncQuotaOverrun then
print(self.VM.LateFrames)
self.VM.SyncQuotaIP = self.VM.IP
self.VM.LateFrames = self.VM.LateFrames + 1
else
self.VM.SyncQuotaIP = nil
self.VM.LateFrames = 0
end
end

-- Reset INTR register for async thread
Expand Down
9 changes: 8 additions & 1 deletion lua/entities/gmod_wire_spu/cl_init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,14 @@ function ENT:Run()
-- Run until interrupt, or if async thread then until async thread stops existing
while (Cycles > 0) and (self.VM.INTR == 0) do
local previousTMR = self.VM.TMR
self.VM:Step()
self.VM.QuotaSupported = 1
self.VM.Quota = self.VM.TMR+Cycles
if self.VM.QuotaOverrunFunc then
self.VM:QuotaOverrunFunc()
else
self.VM:Step()
end
self.QuotaSupported = 0
Cycles = Cycles - (self.VM.TMR - previousTMR)
end

Expand Down
2 changes: 2 additions & 0 deletions lua/wire/cpulib.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,8 @@ CPU(141, "EXTRETPA", 1, 11.00, R0, "PTBL", "", "Set PTBL,
CPU(150, "STERR", 2, 3.00, R0, "X", "Y", "Output an external error where X is error number and Y is error parameter")
CPU(151, "CLERR", 0, 1.00, R0, "", "", "Clear the external error output")
---- Dec 16 -- UNDEFINED ------------------------------------------------------------------------------------------------------------------------
CPU(152, "QUOCMP", 0, 1.00, 0, "", "", "Will set CMPR to 1 if Quota has occurred, or 0 if it hasn't, and reset quota reached flag.")
CPU(153, "QUOTIMER", 1, 1.00, W1, "X", "", "Get the time since a quota-aware instruction was interrupted.")
---- Dec 17 -- UNDEFINED ------------------------------------------------------------------------------------------------------------------------
---- Dec 18 -- UNDEFINED ------------------------------------------------------------------------------------------------------------------------
---- Dec 19 -- UNDEFINED ------------------------------------------------------------------------------------------------------------------------
Expand Down
45 changes: 44 additions & 1 deletion lua/wire/zvm/zvm_core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,47 @@ function ZVM:Dyn_EndBlock()
return self.EmitBlock
end

--------------------------------------------------------------------------------
-- Begins a block of code that will only run if we're in a quota supporting environment
-- and about to exit due to quota.
function ZVM:Dyn_BeginQuotaOnlyCode()
self:Dyn_Emit("if VM.QuotaSupported then")
self:Dyn_Emit("if VM.TMR > VM.Quota then")
end

--------------------------------------------------------------------------------
-- Ends a block of "Quota only" code.
function ZVM:Dyn_EndQuotaOnlyCode()
self:Dyn_Emit("end")
self:Dyn_Emit("end")
thegrb93 marked this conversation as resolved.
Show resolved Hide resolved
end

--------------------------------------------------------------------------------
-- Check if over quota on processors that support it, code between here and EndQuotaInterrupt
-- will be run on next available tick to wrap up what was started by this instruction
--
-- Note that to take arguments, you will have to store your values somewhere in VM and
-- clear them at the end of your function
function ZVM:Dyn_StartQuotaInterrupt()
self:Dyn_Emit("if VM.QuotaSupported then")
self:Dyn_Emit("if VM.TMR > VM.Quota then")
self:Dyn_EmitState()
self:Dyn_Emit("VM.LASTQUO = VM.TIMER+%d*VM.TimerDT",(self.PrecompileInstruction or 0))
self:Dyn_Emit("VM.QUOFLAG = 1")
self:Dyn_Emit("VM.IP = %d",self.PrecompileIP)
self:Dyn_Emit("VM.XEIP = %d",self.PrecompileXEIP)
self:Dyn_Emit("$L function quotafunc(VM)")
end

function ZVM:Dyn_EndQuotaInterrupt()
self:Dyn_EmitState()
self:Dyn_Emit("end")
self:Dyn_Emit("VM.QuotaOverrunFunc = quotafunc")
self:Dyn_Emit("VM.EndedOnQuota = true")
self:Dyn_EmitBreak()
self:Dyn_Emit("end")
self:Dyn_Emit("end")
end

--------------------------------------------------------------------------------
function ZVM:Precompile_Initialize()
Expand All @@ -349,6 +388,7 @@ function ZVM:Precompile_Initialize()
self.PrecompileBreak = false
self.PrecompileInstruction = 0
self.PrecompileBytes = 0
self.PrecompileCurInstructionSize = 0

self.PrecompilePreviousPage = math.floor(self.XEIP / 128)
self:Dyn_StartBlock()
Expand Down Expand Up @@ -379,7 +419,7 @@ function ZVM:Precompile_Fetch()
self.IF = 0
local value = self:ReadCell(self.PrecompileXEIP) or 0
self.IF = prevIF

self.PrecompileCurInstructionSize = self.PrecompileCurInstructionSize + 1
self.PrecompileXEIP = self.PrecompileXEIP + 1
self.PrecompileIP = self.PrecompileIP + 1
self.PrecompileBytes = self.PrecompileBytes + 1
Expand Down Expand Up @@ -408,6 +448,9 @@ function ZVM:Precompile_Step()
-- Reset interrupts trigger if precompiling
self.INTR = 0

-- Allows an opcode to know how big the instruction is.
self.PrecompileCurInstructionSize = 0

-- Check if we crossed the page boundary, if so - repeat the check
if math.floor(self.PrecompileXEIP / 128) ~= self.PrecompilePreviousPage then
self:Emit("VM:SetCurrentPage(%d)",math.floor(self.PrecompileXEIP/128))
Expand Down
2 changes: 2 additions & 0 deletions lua/wire/zvm/zvm_data.lua
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ ZVM.InternalRegister[65] = "TimerRate"
ZVM.InternalRegister[66] = "TimerPrevTime"
ZVM.InternalRegister[67] = "TimerAddress"
ZVM.InternalRegister[68] = "TimerPrevMode"
ZVM.InternalRegister[69] = "LASTQUO"
ZVM.InternalRegister[70] = "QUOFLAG"
----------------------------------
for reg=0,31 do ZVM.InternalRegister[96+reg] = "R"..reg end

Expand Down
10 changes: 5 additions & 5 deletions lua/wire/zvm/zvm_features.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
self.LS = 0

-- Extended registers
for reg=0,31 do self["R"..reg] = 0 end

Check warning on line 60 in lua/wire/zvm/zvm_features.lua

View workflow job for this annotation

GitHub Actions / lint

"Whitespace style"

Style: Please put some whitespace before the operator

Check warning on line 60 in lua/wire/zvm/zvm_features.lua

View workflow job for this annotation

GitHub Actions / lint

"Whitespace style"

Style: Please put some whitespace before the operator

-- Stack size register
self.ESZ = math.max(0,self.RAMSize-1)
Expand Down Expand Up @@ -199,7 +199,7 @@
if self.BusLock == 1 then return end

-- Cycles required to perform memory read
self.TMR = self.TMR + 5
self.TMR = self.TMR + (self.MemoryReadCycles or 5)

-- Check if address is valid
if not IsValidAddress(Address) then
Expand Down Expand Up @@ -242,7 +242,7 @@
value = tonumber(self.Memory[Address]) or 0
else
-- Extra cycles for the external operation
self.TMR = self.TMR + 15
self.TMR = self.TMR + (self.ExternalReadCycles or 15)
value = self:ExternalRead(Address)
end

Expand All @@ -267,7 +267,7 @@
return tonumber(self.Memory[Address]) or 0
else
-- Extra cycles for the external operation
self.TMR = self.TMR + 15
self.TMR = self.TMR + (self.ExternalReadCycles or 15)
return self:ExternalRead(Address)
end
end
Expand All @@ -283,7 +283,7 @@
if self.BusLock == 1 then return false end

-- Cycles required to perform memory write
self.TMR = self.TMR + 5
self.TMR = self.TMR + (self.MemoryWriteCycles or 5)

-- Check if address is valid
if not IsValidAddress(Address) then
Expand Down Expand Up @@ -356,7 +356,7 @@
self.Memory[Address] = Value
else
-- Extra cycles for the external operation
self.TMR = self.TMR + 15
self.TMR = self.TMR + (self.ExternalWriteCycles or 15)
return self:ExternalWrite(Address,Value)
end
end
Expand All @@ -370,7 +370,7 @@
if self.BusLock == 1 then return false end

-- Write to stack
self:WriteCell(self.ESP+self.SS, Value)

Check warning on line 373 in lua/wire/zvm/zvm_features.lua

View workflow job for this annotation

GitHub Actions / lint

"Whitespace style"

Style: Please put some whitespace before the operator
self.ESP = self.ESP - 1

-- Stack check
Expand Down Expand Up @@ -398,7 +398,7 @@
return 0
end

local Value = self:ReadCell(self.ESP+self.SS)

Check warning on line 401 in lua/wire/zvm/zvm_features.lua

View workflow job for this annotation

GitHub Actions / lint

"Whitespace style"

Style: Please put some whitespace before the operator
if Value then return Value
else self:Interrupt(6,self.ESP) return 0 end
end
Expand Down
39 changes: 38 additions & 1 deletion lua/wire/zvm/zvm_opcodes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,8 @@
--------------------------------------------------------------------------------
ZVM.OpcodeTable[70] = function(self) --EXTINT
self:Dyn_EmitState()
self:Emit("VM.IP = %d",(self.PrecompileIP or 0))

Check warning on line 461 in lua/wire/zvm/zvm_opcodes.lua

View workflow job for this annotation

GitHub Actions / lint

"Unnecessary parentheses"

Unnecessary parentheses
self:Emit("VM.XEIP = %d",(self.PrecompileTrueXEIP or 0))

Check warning on line 462 in lua/wire/zvm/zvm_opcodes.lua

View workflow job for this annotation

GitHub Actions / lint

"Unnecessary parentheses"

Unnecessary parentheses
self:Dyn_Emit("VM:ExternalInterrupt(math.floor($1))")
self:Dyn_EmitBreak()
self.PrecompileBreak = true
Expand Down Expand Up @@ -542,14 +542,42 @@
ZVM.OpcodeTable[78] = function(self) --MCOPY
self:Dyn_EmitForceRegisterLocal("ESI")
self:Dyn_EmitForceRegisterLocal("EDI")
self:Dyn_Emit("$L VAL")
self:Dyn_Emit("for i = 1,math.Clamp($1,0,8192) do")
self:Dyn_Emit("$L VAL")
self:Dyn_Emit("VAL = VM:ReadCell(ESI)")
self:Dyn_EmitInterruptCheck()
self:Dyn_Emit("VM:WriteCell(EDI,VAL)")
self:Dyn_EmitInterruptCheck()
-- The code in the QuotaOnlyCode block only executes if we've hit quota.
self:Dyn_BeginQuotaOnlyCode()
self:Dyn_Emit("VM.MCOPYWrapUpCount = math.Clamp($1,0,8192)-i")
self:Dyn_EndQuotaOnlyCode()
self:Dyn_Emit("EDI = EDI + 1")
self:Dyn_Emit("ESI = ESI + 1")
-- The code in the QuotaInterrupt block sets up a function to run and
-- handle the memory copy across multiple frames, to prevent FPS drops.
self:Dyn_StartQuotaInterrupt()
self:Dyn_Emit("$L VAL")
self:Dyn_Emit("for i = 1, math.min(8192,VM.MCOPYWrapUpCount) do")
self:Dyn_Emit("VAL = VM:ReadCell(ESI)")
self:Dyn_EmitInterruptCheck()
self:Dyn_Emit("VM:WriteCell(EDI,VAL)")
self:Dyn_EmitInterruptCheck()
self:Dyn_Emit("EDI = EDI + 1")
self:Dyn_Emit("ESI = ESI + 1")
self:Dyn_BeginQuotaOnlyCode()
self:Dyn_EmitState()
self:Dyn_Emit("VM.MCOPYWrapUpCount = VM.MCOPYWrapUpCount - i")
self:Dyn_Emit("return")
self:Dyn_EndQuotaOnlyCode()
self:Dyn_Emit("end")
self:Dyn_Emit("if VM.MCOPYWrapUpCount <= 8192 then")
self:Dyn_Emit("VM.MCOPYWrapUpCount = nil")
self:Dyn_Emit("VM.QuotaOverrunFunc = nil")
self:Dyn_Emit("else")
self:Dyn_Emit("VM.MCOPYWrapUpCount = VM.MCOPYWrapUpCount - 8192")
self:Dyn_Emit("end")
self:Dyn_EndQuotaInterrupt()
Copy link
Member Author

@DerelictDrone DerelictDrone Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It already does that? It kind of looked to me like it was throwing an interrupt that cancels the instruction entirely, but I guess that's wrong.

Dyn_QuotaOnlyCode details

QuotaOnlyCode will wrap the emits between the start and end with code that will allow it to check if it's going to exit, similar to the Interrupt, but this will allow you to set up things ahead of time that will be needed by the quota interrupt function

On line 551-553 it sets up the variable VM.MCOPYWrapUpCount
On line 565-569 it will emit the state of the CPU, subtract the indexes copied during this execution from the stored variable, and then exit early without changing the quota interrupt func

Dyn_QuotaInterrupt details

If quota is enabled, and the TIMER goes past the QUOTA, which is defined by the context before starting the step as TIMER+CPU Cycles, it will place a function containing all of this code from the dyn_emits between StartQuotaInterrupt and EndQuotaInterrupt in VM.QuotaOverrunFunc, it will also add the size of the current instruction to the instruction pointer, so when it wakes up and starts running VM:Step again it will start from the next instruction, it will also end a block like it's a branching(JMP/CALL/etc.) instruction, so precompile should end on "quota" instructions.

When the CPU sees that VM:QuotaOverrunFunc is not nil, it will run it instead of the regular VM:Step, until VM:QuotaOverrunFunc is nil again.

The function wrapped inside behaves like MCOPY, the diffs are:
Tracks variable VM.MCOPYWrapUpCount to access and store the amount of indexes left to copy.
If it's completed the amount of indexes, it will remove itself as function, and remove the variable it used to track, next tick will return to execution at the next instruction.

Copy link
Member Author

@DerelictDrone DerelictDrone Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intended this to actually point to a wider set of lines than this in the file with my one comment but idk how to correct that.

self:Dyn_Emit("end")
end
ZVM.OpcodeTable[79] = function(self) --MXCHG
Expand Down Expand Up @@ -687,7 +715,7 @@
self:Dyn_Emit("end")
end
ZVM.OpcodeTable[98] = function(self) --TIMER
self:Dyn_EmitOperand("(VM.TIMER+%d*VM.TimerDT)",(self.PrecompileInstruction or 0))

Check warning on line 718 in lua/wire/zvm/zvm_opcodes.lua

View workflow job for this annotation

GitHub Actions / lint

"Unnecessary parentheses"

Unnecessary parentheses
end
ZVM.OpcodeTable[99] = function(self) --LIDTR
self:Dyn_Emit("VM.IDTR = $1")
Expand Down Expand Up @@ -1046,7 +1074,7 @@
ZVM.OpcodeTable[126] = function(self) --LEA
local emitText = self.OperandEffectiveAddress[self.EmitOperandRM[2]] or "0"
emitText = string.gsub(emitText,"$BYTE",self.EmitOperandByte[2] or "0")
emitText = string.format(string.gsub(emitText,"$SEG","VM[%q]",(self.EmitOperandSegment[2] or "DS")))

Check warning on line 1077 in lua/wire/zvm/zvm_opcodes.lua

View workflow job for this annotation

GitHub Actions / lint

"Unnecessary parentheses"

Unnecessary parentheses
self:Dyn_EmitOperand(emitText)
end
ZVM.OpcodeTable[127] = function(self) --BLOCK
Expand Down Expand Up @@ -1188,7 +1216,7 @@
self:Dyn_Emit("V = VM:Pop()") -- IRET EIP
self:Dyn_EmitInterruptCheck()

for i=0,31 do

Check warning on line 1219 in lua/wire/zvm/zvm_opcodes.lua

View workflow job for this annotation

GitHub Actions / lint

"Whitespace style"

Style: Please put some whitespace before the operator
self:Dyn_Emit("V = VM:Pop()") self:Dyn_EmitInterruptCheck() self:Dyn_Emit("VM.R%d = V")
end

Expand Down Expand Up @@ -1231,6 +1259,15 @@
self:Dyn_Emit("VM:SignalError(0)")
end

ZVM.OpcodeTable[152] = function(self) -- QUOCMP
self:Dyn_Emit("VM.CMPR = VM.QUOFLAG")
self:Dyn_Emit("VM.QUOFLAG = 0")
end

ZVM.OpcodeTable[153] = function(self) -- QUOTIMER
self:Dyn_EmitOperand("VM.LASTQUO")
end

--------------------------------------------------------------------------------
ZVM.OpcodeTable[250] = function(self) --VADD
local seg1code = self.EmitOperandSegment[1] and "0" or "VM.DS"
Expand Down Expand Up @@ -1641,7 +1678,7 @@
self:Dyn_EmitInterruptCheck()
end
ZVM.OpcodeTable[269] = function(self) --VLEN
local seg1code = self.EmitOperandSegment[1] and "0" or "VM.DS"

Check warning on line 1681 in lua/wire/zvm/zvm_opcodes.lua

View workflow job for this annotation

GitHub Actions / lint

"Unused variable"

Unused variable: seg1code
local seg2code = self.EmitOperandSegment[2] and "0" or "VM.DS"
self:Dyn_Emit("if VM.VMODE == 2 then")
self:Dyn_Emit("$L V = VM:ReadVector2f($2 + %s)",seg2code)
Expand Down
Loading