ÎÌÌÌ̈ˆˆ¬ªªª¬ˆˆˆ¬ÌÌ̬ÌÀ̬ÌÀ̬ÌÀÌÌÌìÌÀÀî̬ÀÌÀ¬ÀÀÀ¬ÀÀÀ¬ÀÀÎÌÌÌ̈ˆˆ¬ªªª¬ˆˆˆ¬ÌÌ̬ÌÌ̬ÌÀ̬ÌÀÌÌÌìÌÀÀî̬ÀÌ̬ÀÀÀ¬ÀÀÀ¬ÀÀ¬ÌÌ̬ªªª¬ª¬ª¬ªÊ̬ªªªŒˆˆˆÌÀÌÎÌÌÎ̬̪ªÀ쬪ÀÀÀÌîÀìîÌÌîî¬ÌÌ̬ªªª¬ª¬ª¬ªÊ̬ªªªŒˆˆˆÌÀÌÎÌÌÎ̬̪ªÀ쬪ÀÀÀÌîÀìîÌÌîî >·8àîàîîààîàîààîîàîîàààààîàààîààààààààààààààààààààààààààààààààààààààîàààààààààààààîààààààààààààààààààààîàààààààààààîîààîààîàààîàààîàààîàîààîîàîàààÀ ÀÀÀÌ ÀÌÀÌÀÀÌ ÌÌÌÀÀÀ À ÌÌÌÀÌ ÌÌÀÀÀÀ ÀÀ ÀÀÀÀÀ ÀÀÀÀÀÀÀ ÀÀÌÌ ÀÀ ÀÀÀÀÀÀÀÌÀÀÀÀÀÀÀÌÀÀ ÀÀÀ À ÀÀ ÀÀÀÀÀÀÀÀÀÀÀÀÀ À ÀÀ ÀÌ ÀÀÀÀÀÀÀÀÀÀÀÀÀ ÀÀÀÀ ÀÀ ÀÀÀÀ ÀÀÀÀÀÀÀÀÀÀÀÀÀ ÀÀ À ÀÀÀÀÀÀÀÀ ÌÌÀÀÌ ÀÀ ÀÌÀÌÀÀÌÀ À À ÀÀ ÀÀÀ ÌÀÌ ÀÌÀ À À ÀÀ ÀÀÀÀ ÀÀÌÌÀ À À ÀÀÌÀÀÀ Ì ÀÀÀÀÀÀÌÌÀÀÀ ÀÌ À ÌÀÀÀ À ÀÀÀ ÀÀÀÀ ÀÌÀÀÀÀ ÀÀÀ Ì ÀÀÀÀÀÀ À ÀÀ À À ÀÀÀÀ ÀÀÀÀÀÀ À ÀÀÀ ÀÀÀÀÀÀÀÀÀ ÀÀÌÀÀÀÌÀÌÀÀÀÌÌÀÀÌÀÀÌÀÀ ÀÀÀ À À ÌÌÌÀÀÀÌÀÌÀÌÀ ÀÌÀÌÌÌÀ ÌÌÀÀÌÀÀÀÌÀÀÀÀÀÀÀÀÌÌÀ ÀÀÀÀÀÀ ÀÀÀÌÀÀÀ ÀÀÀÀÀÀÀÀÀ À ÀÀÀÌÀÀ À ÀÌ ÀÀÀÀ ÀÌ ÀÀÌÀ ÀÌÀÀÀÀÀÀÀÀÀÀ ÀÀ ÀÀÀÀÀÀÀÀÀÀÀÀÀ À ÀÀÀÌÀÀÀÀÀÀÀÀÀÀÀÌÀÀÌÌÀÀÀ ÌÀÀÀ ÀÀÀ ÀÀ ÌÌÀÌÀÀÀÌÀÌÀÀÀÌÀ À À ° °»° »»»»° ° »°»°°° °°°°°°°°°°»° °°» ° ° °»°°°° °°° °°°° °°°°°°°°°°°»»°»°»°»°°° °»° °° ° °»°»»°°°°° °»°»°° °»° °° »°°°°°°°°»»°°° °°°°° °°° °°°°°°°°°°» ° °»° ° »°°°» °» °°°°°°°»°°°°°°°°°°°»° °° °»»° »°°°°»°° °°° °°»°»°°°»°°» °°»°»»° ° ° °»°°° »° ° ° »» °°»°°°°°°°°°°»°°°°°°°° ° ° °»°°°°°°°°°°°° °° °°°°°°°°»°°»°»° ° »» °»»° °»° ° °°»°°°°°°»° »° °» °°°° °°°»° °°° °»»° °°°° °»°» ° »° ° ° °°°°»°°°° °»° »°»°° °° °°°» ° » » °°°°°°°°°°° °°°°°° °°° °°° » » °°»»°» °° °°»°°»°»°° ° °»°° ° » »° ° ° °°°» °° ° °° °»°°°»°»° °° °°»° ° °°°»°°°° ° °»°»°°°° °°° °°°°°°°°°°°° °» °° ° °°°° °°°°°°°°»°°°°°° °° °°°° °°°°°° »°°°° »°°»°°»°°»°°° °»»°°°° ° °»°»°°° ° ° °»°°°°°°»° °°»»»°»°°°°° »° »°° °»° °°°°°»»°»°°°»»°°°°°°°°°° ° °°°°»°°° °°°°°» °»°»° °°°°°°°°°°°°°°»° °°°°°°° °°°°°°°°°°° °°°°°°°»° °°°»° »»°»°°»° °°°°°»°»»° °°°»°»°° °°°° ÀÀÀÀ À À À ÀÀÀ À ÀÀÌÌÀ ÀÀÀÀÀÀ ÀÀÀÌÀ ÀÀÀÌ ÌÌÀÌÀ ÀÌÀ ÀÌ ÌÌÌÀÀÌÀÀ ÀÀÀÀÀÀÀÀÀÀÀ À ÌÀÀ Ì ÀÀÀ ÀÀÀ ÀÀÀÀ ÀÀÀÌÀÀÀÀÀÀÀ ÀÀÀÀÀÀÀ ÀÀÀÀÀÀÀÀÀ ÀÀÀÀÀÌ ÀÀÀÀÀÀÀÀ À À ÀÀÀÀ ÀÀÀÀÀÀÀÀÀ ÀÀ ÀÌÀÀÌÌÀÀ ÀÀÀÀÌÀÌÀÀÌÀÀÌ ÀÌÀÌÌÌÀÀ ÀÀÀÀÀ ÌÌÀ ÀPPPPPPPPPUPUPUPPUUPPPUPPPUPPPPPPPPPPPPUPPPUPPPPPPPPPPUPPUUUPPPPPPPPPPPPPPPPPPPUPPPUPPUPPPUPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPUPPPPPPUPPPUPUPUPUPPPPUUPPPUPUUPUPPUUPPUPPPPUUUPUPPPPPPUUPPUPPPUUPPPPPPPUPPUPPPPPPUPPPPPPPPPPPPPPPPPPPPPPPPPPPPUPUPPPPPUUPUPPPPUUUPPPPPPPPPPUPPUUPPUPPPUPUUPPPPUPUPUUPUPPPPPPUPPPPPPPUPUPUPPPPPPPPPPPUPPPPUPPPPPPPPPPUPPUPUPPPPPPPPPPPPUPPPPPPPPPPPPPPPPPPPPPPPPPPUPPPPPPPPPPPPPPPPUPPPPPPPPPPPPUPPUPPUPPPUPUPPUPPUPPPPPUPPPPUPPUPPUUPUPPPPUPUUPPUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUP»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»P»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»P»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»PUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUè_-- title: TIC-80 Cross-Runtime Benchmark -- author: tenfour -- desc: Compare stock web TIC-80 with the ticbuild desktop fork -- script: lua --[[ Cross-runtime benchmark goals ----------------------------- * Same deterministic cart/workload on desktop and web. * Actual TIC callback rate over a 0.5 second rolling window. * Inside-TIC execution time and theoretical uncapped TIC/s. * Lua VM instructions/frame, counted with a 1000-instruction hook on stock builds. * Native profiler auto-detection for thenfour/TIC-80-ticbuild. AUTO mode: stock TIC-80 -> installs this cart's 1000-instruction Lua hook ticbuild fork -> leaves the fork's native C hook in control; use ALT+0 there SELF mode always requests the cart's Lua hook. On ticbuild this is forwarded by its native hook, so SELF is useful diagnostically but has more observer overhead. RAW mode disables the cart hook. On ticbuild it also disables the native count hook at the beginning of each TIC, because ticbuild reinstalls it every frame. The host-side cycle count is intentionally invalid in RAW mode. --]] local VERSION = "1.0" local SCREEN_W = 240 local SCREEN_H = 136 local SCREEN_PIXELS = SCREEN_W * SCREEN_H local HOOK_STEP = 1000 local FPS_WINDOW_MS = 500 local FPS_MAX_SAMPLES = 256 local DISPLAY_PERIOD_MS = 200 -- 5 Hz, matching the ticbuild HUD local FRAME_BUDGET_MS = 1000 / 60 local WARMUP_FRAMES = 45 local EMA_UNCAPPED = 0.20 local EMA_DEFAULT = 0.25 local PAIR_EMA = 0.10 local MODE_AUTO = 1 local MODE_RAW = 2 local MODE_SELF = 3 local MODE_COUNT = 3 local PROF_NATIVE = 1 local PROF_SELF = 2 local PROF_RAW = 3 local MASK31 = 0x7fffffff local MASK16 = 0xffff local FIXED_SEED = 0x13579bdf -- The custom fork exposes perf_tick/perf_clear to carts. Official TIC-80 does not. local nativeProfiler = type(perf_tick) == "function" local initialHook, initialHookMask, initialHookCount = debug.gethook() local selfInstructionCounter = 0 local function selfInstructionHook() selfInstructionCounter = selfInstructionCounter + HOOK_STEP end local mode = MODE_AUTO local previousMode = nil local paused = false local settingsChangedThisFrame = false local snapshotNoticeUntil = 0 local levelValues = { 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000, 2000000, 5000000, } local tableA = {} local tableB = {} local function kernelIdle(_) return FIXED_SEED end local function kernelAlu(n) local a = FIXED_SEED local b = 0x02468ace local c = 0x01020304 for i = 1, n do a = (a + b + i) & MASK31 b = ((b ~ a) + c) & MASK31 c = (c + ((a >> 3) ~ (b << 1))) & MASK31 if (a & 8) == 0 then b = (b + c) & MASK31 else a = (a + c) & MASK31 end end return (a ~ b ~ c) & MASK31 end local function kernelTable(n) -- Reset a fixed, preallocated data set so every frame has the same result. for i = 1, 256 do tableA[i] = (i * 257 + 0x1234) & MASK16 tableB[i] = (i * 911 + 0x5678) & MASK16 end local acc = FIXED_SEED for i = 1, n do local ia = (i & 255) + 1 local ib = ((i * 17) & 255) + 1 local va = tableA[ia] local vb = tableB[ib] local nv = (va + vb + acc) & MASK16 tableA[ia] = nv tableB[ib] = (vb ~ (acc & MASK16)) & MASK16 acc = (acc + nv + vb) & MASK31 end return (acc ~ tableA[1] ~ tableB[256]) & MASK31 end local function callStep(a, b, c, i) a = (a + b + i) & MASK31 b = ((b ~ c) + a) & MASK31 c = (c + ((a >> 5) ~ b)) & MASK31 if (i & 1) == 0 then return b, c, a end return c, a, b end local function kernelCalls(n) local a = FIXED_SEED local b = 0x10293847 local c = 0x56473829 for i = 1, n do a, b, c = callStep(a, b, c, i) end return (a ~ b ~ c) & MASK31 end local function kernelApi(n) local addr = 0 local acc = FIXED_SEED for i = 1, n do addr = addr + 73 if addr >= SCREEN_PIXELS then addr = addr - SCREEN_PIXELS end local color = (acc + i) & 15 poke4(addr, color) if (i & 15) == 0 then acc = (acc + peek4(addr) + addr) & MASK31 else acc = (acc + color) & MASK31 end end return acc end local function kernelMixed(n) for i = 1, 64 do tableA[i] = (i * 313 + 7) & MASK16 end local a = FIXED_SEED local b = 0x24681357 local c = 0x10203040 local addr = 0 for i = 1, n do local index = (i & 63) + 1 local v = tableA[index] a = (a + v + i) & MASK31 b = ((b ~ a) + v) & MASK31 tableA[index] = (v + (b & 255)) & MASK16 if (i & 3) == 0 then a, b, c = callStep(a, b, c, i) else c = (c + a + (b >> 4)) & MASK31 end if (i & 31) == 0 then addr = addr + 137 if addr >= SCREEN_PIXELS then addr = addr - SCREEN_PIXELS end poke4(addr, a & 15) end end return (a ~ b ~ c ~ tableA[1] ~ tableA[64]) & MASK31 end -- Scale makes one load level useful across kernels with very different costs. local kernels = { {name = "IDLE", num = 0, den = 1, run = kernelIdle}, {name = "ALU", num = 1, den = 1, run = kernelAlu}, {name = "TABLE", num = 1, den = 1, run = kernelTable}, {name = "CALL", num = 1, den = 2, run = kernelCalls}, {name = "API", num = 1, den = 10, run = kernelApi}, {name = "MIXED", num = 1, den = 4, run = kernelMixed}, } local kernelIndex = 2 -- ALU local levelIndex = 6 -- 50,000 base units local checksum = FIXED_SEED local function currentIterations() local kernel = kernels[kernelIndex] if kernel.num == 0 then return 0 end return math.floor(levelValues[levelIndex] * kernel.num / kernel.den) end local function effectiveProfiler() if mode == MODE_RAW then return PROF_RAW end if mode == MODE_SELF then return PROF_SELF end if nativeProfiler then return PROF_NATIVE end return PROF_SELF end local function modeName() if mode == MODE_AUTO then if nativeProfiler then return "AUTO>NATIVE" end return "AUTO>SELF1000" elseif mode == MODE_SELF then return "SELF1000" end return "RAW" end local function applyProfilerMode() -- Do not disturb ticbuild's native C hook in AUTO mode. if mode == MODE_AUTO and nativeProfiler then if previousMode == MODE_SELF then -- Remove our previous Lua hook. ticbuild restores its native hook next frame. debug.sethook() end elseif mode == MODE_SELF or (mode == MODE_AUTO and not nativeProfiler) then debug.sethook(selfInstructionHook, "", HOOK_STEP) else debug.sethook() end previousMode = mode end -- 0.5-second rolling FPS tracker, deliberately mirroring ticbuild_remoting/fps.c. local fpsDt = {} local fpsHead = 1 local fpsCount = 0 local fpsSumMs = 0 local fpsLastEntryMs = nil local actualFps = 0 local function resetFps() fpsHead = 1 fpsCount = 0 fpsSumMs = 0 fpsLastEntryMs = nil actualFps = 0 end local function pushFrameDelta(dt) if dt <= 0 then return end if fpsCount < FPS_MAX_SAMPLES then local index = ((fpsHead - 1 + fpsCount) % FPS_MAX_SAMPLES) + 1 fpsDt[index] = dt fpsCount = fpsCount + 1 fpsSumMs = fpsSumMs + dt else fpsSumMs = fpsSumMs - fpsDt[fpsHead] fpsDt[fpsHead] = dt fpsSumMs = fpsSumMs + dt fpsHead = (fpsHead % FPS_MAX_SAMPLES) + 1 end while fpsCount > 1 and fpsSumMs > FPS_WINDOW_MS do fpsSumMs = fpsSumMs - fpsDt[fpsHead] fpsHead = (fpsHead % FPS_MAX_SAMPLES) + 1 fpsCount = fpsCount - 1 end if fpsCount > 0 and fpsSumMs > 0 then actualFps = fpsCount * 1000 / fpsSumMs end end local function fpsOnFrame(entryMs) if fpsLastEntryMs ~= nil then pushFrameDelta(entryMs - fpsLastEntryMs) end fpsLastEntryMs = entryMs end local emaTicMs = 0 local emaUncappedFps = 0 local emaInstructions = 0 local emaMips = 0 local emaTicInitialized = false local emaUncappedInitialized = false local emaInstructionsInitialized = false local emaMipsInitialized = false local lastRawTicMs = 0 local lastRawInstructions = 0 local lastRawMips = 0 local warmupRemaining = WARMUP_FRAMES local pairRecords = {} local function emaUpdate(old, value, alpha, initialized) if not initialized then return value, true end return old + alpha * (value - old), true end local function settingKey(k, l) return k * 100 + l end local function resetLiveStats() emaTicMs = 0 emaUncappedFps = 0 emaInstructions = 0 emaMips = 0 emaTicInitialized = false emaUncappedInitialized = false emaInstructionsInitialized = false emaMipsInitialized = false lastRawTicMs = 0 lastRawInstructions = 0 lastRawMips = 0 warmupRemaining = WARMUP_FRAMES resetFps() end local function updatePairRecord(key, profiler, ticMs, instructions) if warmupRemaining > 0 or paused then return end local record = pairRecords[key] if record == nil then record = {} pairRecords[key] = record end if profiler == PROF_SELF then if record.selfMs == nil then record.selfMs = ticMs else record.selfMs = record.selfMs + PAIR_EMA * (ticMs - record.selfMs) end if instructions ~= nil then if record.selfInstructions == nil then record.selfInstructions = instructions else record.selfInstructions = record.selfInstructions + PAIR_EMA * (instructions - record.selfInstructions) end end elseif profiler == PROF_RAW then if record.rawMs == nil then record.rawMs = ticMs else record.rawMs = record.rawMs + PAIR_EMA * (ticMs - record.rawMs) end elseif profiler == PROF_NATIVE then if record.nativeMs == nil then record.nativeMs = ticMs else record.nativeMs = record.nativeMs + PAIR_EMA * (ticMs - record.nativeMs) end end end -- The previous sample is finalized at the beginning of the next TIC. That puts -- statistics work inside the following frame's measured body rather than after -- the ending timestamp, keeping the unmeasured tail extremely small. local pendingValid = false local pendingStartMs = 0 local pendingEndMs = 0 local pendingInstructionStart = 0 local pendingInstructionEnd = 0 local pendingCountsInstructions = false local pendingProfiler = PROF_RAW local pendingSettingKey = 0 local pendingPaused = false local function finalizePendingSample() if not pendingValid then return end local ticMs = pendingEndMs - pendingStartMs if ticMs < 0 then pendingValid = false return end local uncapped = 0 if ticMs > 0 then uncapped = 1000 / ticMs end local instructions = nil local mips = nil if pendingCountsInstructions then instructions = pendingInstructionEnd - pendingInstructionStart if instructions < 0 then instructions = 0 end if ticMs > 0 then mips = instructions / ticMs / 1000 else mips = 0 end end lastRawTicMs = ticMs lastRawInstructions = instructions or 0 lastRawMips = mips or 0 emaTicMs, emaTicInitialized = emaUpdate( emaTicMs, ticMs, EMA_DEFAULT, emaTicInitialized) emaUncappedFps, emaUncappedInitialized = emaUpdate( emaUncappedFps, uncapped, EMA_UNCAPPED, emaUncappedInitialized) if instructions ~= nil then emaInstructions, emaInstructionsInitialized = emaUpdate( emaInstructions, instructions, EMA_DEFAULT, emaInstructionsInitialized) emaMips, emaMipsInitialized = emaUpdate( emaMips, mips, EMA_DEFAULT, emaMipsInitialized) end if warmupRemaining > 0 then warmupRemaining = warmupRemaining - 1 end if not pendingPaused then updatePairRecord(pendingSettingKey, pendingProfiler, ticMs, instructions) end pendingValid = false end local displayLines = {} local nextDisplayUpdateMs = 0 local function formatInteger(value) local s = tostring(math.floor(value + 0.5)) local result = "" local first = #s % 3 if first == 0 then first = 3 end result = string.sub(s, 1, first) for i = first + 1, #s, 3 do result = result .. "," .. string.sub(s, i, i + 2) end return result end local function formatInstructions(value) if value >= 1000000 then return string.format("%.3f M", value / 1000000) elseif value >= 1000 then return string.format("%.1f k", value / 1000) end return string.format("%.0f", value) end local function hookDescription() local hook, mask, count = debug.gethook() local kind if hook == nil then kind = "none" elseif hook == selfInstructionHook then kind = "self" elseif type(hook) == "string" then kind = "external" else kind = "other" end return string.format("%s %s/%d", kind, mask or "", count or 0) end local function currentInstructionDisplay(record, profiler) if profiler == PROF_SELF then if emaInstructionsInitialized then return emaInstructions, "measured" end return nil, "warming" end if profiler == PROF_RAW then if record and record.selfInstructions then return record.selfInstructions, "estimated" end return nil, "none" end return nil, "native" end local function refreshResultAndDisplay(nowMs) if nowMs < nextDisplayUpdateMs then return end nextDisplayUpdateMs = nowMs + DISPLAY_PERIOD_MS local kernel = kernels[kernelIndex] local iterations = currentIterations() local profiler = effectiveProfiler() local key = settingKey(kernelIndex, levelIndex) local record = pairRecords[key] local shownInstructions, instructionSource = currentInstructionDisplay(record, profiler) local shownMips = nil if shownInstructions ~= nil and emaTicMs > 0 then shownMips = shownInstructions / emaTicMs / 1000 end local hostName = nativeProfiler and "TICBUILD" or "STOCK/WEB" local pauseText = paused and " PAUSED" or "" displayLines[1] = string.format( "TICBENCH %s %s %s", VERSION, hostName, _VERSION) displayLines[2] = string.format( "mode %-13s hook %s", modeName(), hookDescription()) displayLines[3] = string.format( "kernel %-5s iterations %s%s", kernel.name, formatInteger(iterations), pauseText) displayLines[4] = string.format( "checksum 0x%08x warmup %d", checksum, warmupRemaining) displayLines[5] = string.format("actual FPS %8.2f", actualFps) displayLines[6] = string.format( "TIC body %8.3f ms raw %.3f", emaTicMs, lastRawTicMs) displayLines[7] = string.format("uncapped TIC/s %8.1f", emaUncappedFps) if shownInstructions ~= nil then local marker = instructionSource == "estimated" and "~" or " " displayLines[8] = string.format( "Lua VM/frame %s%-8s (%s)", marker, formatInstructions(shownInstructions), instructionSource) displayLines[9] = string.format( "VM throughput %7.1f M/s", shownMips or 0) elseif profiler == PROF_NATIVE then displayLines[8] = "Lua VM/frame native HUD: ALT+0" displayLines[9] = "VM throughput cycles / TIC ms" else displayLines[8] = "Lua VM/frame unavailable" displayLines[9] = "VM throughput run SELF first" end local budgetPercent = emaTicMs / FRAME_BUDGET_MS * 100 local headroom = FRAME_BUDGET_MS - emaTicMs displayLines[10] = string.format( "60Hz budget %7.1f%% headroom %.2f ms", budgetPercent, headroom) local taxText = "paired hook/raw tax: collect both modes" local taxValue = nil local taxKind = "none" if record then if record.selfMs and record.rawMs and record.rawMs > 0 then taxValue = (record.selfMs / record.rawMs - 1) * 100 taxKind = "self_vs_raw" taxText = string.format( "SELF hook tax %+7.1f%%", taxValue) elseif record.nativeMs and record.rawMs and record.rawMs > 0 then taxValue = (record.nativeMs / record.rawMs - 1) * 100 taxKind = "native_vs_raw" taxText = string.format( "native hook tax %+7.1f%%", taxValue) end end displayLines[11] = taxText displayLines[12] = "UP/DN kernel LEFT/RIGHT load Z mode" displayLines[13] = "X reset A pause S trace" if nowMs < snapshotNoticeUntil then displayLines[14] = "snapshot written with trace()" elseif nativeProfiler and profiler == PROF_NATIVE then displayLines[14] = "desktop comparison: enable host HUD with ALT+0" elseif profiler == PROF_RAW and nativeProfiler then displayLines[14] = "RAW: native host cycle counter intentionally disabled" elseif profiler == PROF_RAW then displayLines[14] = "RAW: cycle estimate comes from last SELF run" else displayLines[14] = "same kernel + iterations = comparable workload" end TICBENCH_RESULT.version = VERSION TICBENCH_RESULT.host = hostName TICBENCH_RESULT.lua = _VERSION TICBENCH_RESULT.mode = modeName() TICBENCH_RESULT.kernel = kernel.name TICBENCH_RESULT.iterations = iterations TICBENCH_RESULT.checksum = checksum TICBENCH_RESULT.actual_fps = actualFps TICBENCH_RESULT.tic_ms = emaTicMs TICBENCH_RESULT.raw_tic_ms = lastRawTicMs TICBENCH_RESULT.uncapped_fps = emaUncappedFps TICBENCH_RESULT.instructions = shownInstructions TICBENCH_RESULT.raw_instructions = lastRawInstructions TICBENCH_RESULT.instruction_source = instructionSource TICBENCH_RESULT.mips = shownMips TICBENCH_RESULT.raw_mips = lastRawMips TICBENCH_RESULT.profiler = profiler == PROF_NATIVE and "native" or (profiler == PROF_SELF and "self" or "raw") TICBENCH_RESULT.hook_step = HOOK_STEP TICBENCH_RESULT.hook_tax_percent = taxValue TICBENCH_RESULT.hook_tax_kind = taxKind TICBENCH_RESULT.paired_self_ms = record and record.selfMs or nil TICBENCH_RESULT.paired_native_ms = record and record.nativeMs or nil TICBENCH_RESULT.paired_raw_ms = record and record.rawMs or nil TICBENCH_RESULT.paired_self_instructions = record and record.selfInstructions or nil TICBENCH_RESULT.raw_equivalent_mips = record and record.selfInstructions and record.rawMs and record.rawMs > 0 and record.selfInstructions / record.rawMs / 1000 or nil TICBENCH_RESULT.frame_budget_percent = budgetPercent TICBENCH_RESULT.headroom_ms = headroom TICBENCH_RESULT.warmup_frames = warmupRemaining end local function drawHud() cls(0) local y = 3 for i = 1, 14 do local color = 12 if i == 1 then color = 14 elseif i >= 5 and i <= 10 then color = 11 elseif i >= 12 then color = 5 end print(displayLines[i] or "", 3, y, color, false, 1, true) y = y + 8 end -- Compact visual indication of 60 Hz frame-budget usage. local barX = 3 local barY = 116 local barW = 234 local used = 0 if emaTicInitialized then used = math.floor(math.min(1, emaTicMs / FRAME_BUDGET_MS) * (barW - 2)) end rectb(barX, barY, barW, 5, 5) if used > 0 then rect(barX + 1, barY + 1, used, 3, emaTicMs <= FRAME_BUDGET_MS and 11 or 6) end end local function clearCurrentPairRecord() pairRecords[settingKey(kernelIndex, levelIndex)] = nil end local function markSettingsChanged(clearPair) settingsChangedThisFrame = true pendingValid = false resetLiveStats() nextDisplayUpdateMs = 0 if clearPair then clearCurrentPairRecord() end collectgarbage("collect") end local function handleControls() if btnp(0) then kernelIndex = kernelIndex - 1 if kernelIndex < 1 then kernelIndex = #kernels end markSettingsChanged(false) elseif btnp(1) then kernelIndex = kernelIndex + 1 if kernelIndex > #kernels then kernelIndex = 1 end markSettingsChanged(false) end if btnp(2) then levelIndex = math.max(1, levelIndex - 1) markSettingsChanged(false) elseif btnp(3) then levelIndex = math.min(#levelValues, levelIndex + 1) markSettingsChanged(false) end if btnp(4) then mode = (mode % MODE_COUNT) + 1 applyProfilerMode() markSettingsChanged(false) end if btnp(5) then markSettingsChanged(true) end if btnp(6) then paused = not paused markSettingsChanged(false) end end local function traceSnapshot(nowMs) -- Force a fresh text/result snapshot even if the normal 5 Hz display -- refresh interval has not elapsed yet. nextDisplayUpdateMs = 0 refreshResultAndDisplay(nowMs) local r = TICBENCH_RESULT local instructions = r.instructions or -1 local mips = r.mips or -1 local text = string.format( "TICBENCH version=%s host=%s lua=%s mode=%s kernel=%s iterations=%d " .. "checksum=0x%08x fps=%.3f tic_ms=%.4f uncapped=%.3f " .. "instructions=%.0f instruction_source=%s mips=%.3f " .. "paired_self_ms=%.4f paired_native_ms=%.4f paired_raw_ms=%.4f " .. "paired_self_instructions=%.0f raw_equivalent_mips=%.3f " .. "hook_tax_pct=%.3f hook_tax_kind=%s", r.version or VERSION, r.host or "?", r.lua or "?", r.mode or "?", r.kernel or "?", r.iterations or 0, r.checksum or 0, r.actual_fps or 0, r.tic_ms or 0, r.uncapped_fps or 0, instructions, r.instruction_source or "none", mips, r.paired_self_ms or -1, r.paired_native_ms or -1, r.paired_raw_ms or -1, r.paired_self_instructions or -1, r.raw_equivalent_mips or -1, r.hook_tax_percent or -1, r.hook_tax_kind or "none") trace(text, 14) snapshotNoticeUntil = nowMs + 1500 nextDisplayUpdateMs = 0 end -- Public, remoting-friendly result table. The ticbuild fork can read this with -- evalexpr/typeschema while the same cart remains valid on stock web TIC-80. TICBENCH_RESULT = { version = VERSION, initial_hook_type = initialHook == nil and "none" or type(initialHook), initial_hook_mask = initialHookMask or "", initial_hook_count = initialHookCount or 0, native_profiler = nativeProfiler, } function BOOT() for i = 1, FPS_MAX_SAMPLES do fpsDt[i] = 0 end for i = 1, 256 do tableA[i] = 0 tableB[i] = 0 end collectgarbage("collect") applyProfilerMode() resetLiveStats() nextDisplayUpdateMs = 0 end function TIC() -- ticbuild reinstalls its native hook before every TIC. RAW explicitly -- disables it here; this happens before our inside-TIC timestamp. if mode == MODE_RAW and nativeProfiler then debug.sethook() end local frameStartMs = time() local instructionStart = selfInstructionCounter settingsChangedThisFrame = false finalizePendingSample() fpsOnFrame(frameStartMs) handleControls() local iterations = currentIterations() if not paused then checksum = kernels[kernelIndex].run(iterations) end refreshResultAndDisplay(frameStartMs) drawHud() if btnp(7) then traceSnapshot(frameStartMs) end local profiler = effectiveProfiler() pendingStartMs = frameStartMs pendingInstructionStart = instructionStart pendingCountsInstructions = profiler == PROF_SELF pendingProfiler = profiler pendingSettingKey = settingKey(kernelIndex, levelIndex) pendingPaused = paused pendingValid = not settingsChangedThisFrame pendingEndMs = time() pendingInstructionEnd = selfInstructionCounter end