Repository navigation
runtime: per-frame render event as a frame-counter signal (alternative to #89) - #93
Merged
Merged
Conversation
addEventListener("render", (frame) => ...) runs at most once per rendered
frame, in and out of game, so scripts can move or restyle drawables every
frame. GameLoop::OnDraw only increments a global frame counter
(events::RenderFrames) before Drawable::DrawAll; it queues nothing and never
waits on a script. Each pass of a script's event loop (Script::ExecuteEvents,
inside delay()) fires its render listeners once when the counter moved since
it last did, so a slow script sees a bigger jump in frame rather than a
backlog, and no script listening costs nothing beyond the increment.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
RenderFrames becomes FrameCounter (a uint64_t count, Increment / Current) and RenderEventDispatch goes: GameLoop::OnDraw increments the counter itself. Script::FireRenderEventIfNewFrame fires when the current frame is past the one it last fired for and the script listens for "render", and only then records it, so a listener added later fires on the next pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ResurrectedTrader
force-pushed
the
runtime-render-signal
branch
from
October 8, 2026 19:41
0fd2b93 to
f2e5548
Compare
A render handler that calls delay() runs the event loop again, and a frame drawn during that delay would fire render inside itself, one level deeper every frame until the stack runs out. A per-thread flag (each script has its own thread) skips render while its handler runs; the frames drawn meanwhile fold into the next call. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An alternative to #89 with the same user-facing
renderevent, done as a signal instead of a queued event. Pick one of the two; they conflict.Design
GameLoop::OnDraw(the backend'sonDrawcallback, once per rendered frame, in and out of game) callsevents::FrameCounter::Increment(): one relaxed atomic increment, beforeDrawable::DrawAll. It queues nothing, takes no lock, and never touches a script.Script::ExecuteEvents, whichdelay()runs) callsScript::FireRenderEventIfNewFrame()right after pumping the V8 task queue. IfFrameCounter::Current()is past the frame this script last fired for (lastRenderFrame_) and the script has arenderlistener, it fires the listeners once, synchronously, with that frame number, and only then records it.IdleSleepIntervalMs(default 10 ms) to service cross-thread posted events, so a waiting script sees a new frame with that same latency, without busy-polling.Coalescing comes for free. A slow script fires once and sees a bigger jump in
frame. There is no backlog, and no per-script pending state to manage.How it differs from #89
RenderLatch(arm / disarm / deliver, the per-script delivery timestamp) is gone. The per-script state is oneuint64_t lastRenderFrame_, touched only on the script thread.ListenerCountprobe, noForEachScriptwalk, no event allocation, noPostEvent, and no failure path. runtime: add a per-frame render script event #89 walks the scripts and posts an event to each listener every frame.frame, the latest rendered frame number.elapsedis dropped; scripts can usegetTickCount().renderfires only from the event loop (delay()and the otherExecuteEventswaits). runtime: add a per-frame render script event #89's posted event is also run only byPumpMessageLoopinside the same loop, so scripts see no difference.Tests
FrameCounterTest: counting rendered frames.GameLoopTest(reused from runtime: add a per-frame render script event #89):OnDrawraises the render dispatch once per frame, before the drawables are drawn.Verification
build.ps1 Release(Win32): 0 warnings, 0 errors.build.ps1 test: 174/174 test cases passed.build.ps1 check-format: clean.extract_api.pyand thengen_dts.pyemitaddEventListener(event: "render", callback: (frame: number) => void), andtsc --noEmit --lib es2020on the d.ts exits 0.Not live-tested in game. This is #89's example script, using
getTickCount()in place ofelapsed:🤖 Generated with Claude Code