standalone game loop: GC support + per-frame get_dt#3245
Merged
Conversation
Standalone `daslang game.das` runs its own `def main` loop, where the daslang-live host's clock and GC are absent. Two gaps surface there: - get_dt() was a per-call stopwatch, so calling it twice in one frame split the frame's delta across the calls. Compute the frame delta once per frame in live_begin_frame (live_advance_frame_clock) and cache it; get_dt() now returns that stable value. No-op under the live host, which still owns the clock. - the heap is never collected. Add maybe_collect_gc() to glfw_live: it collects a heap only when it is mostly free (cheap compaction), so it is fine to call every frame after update/render. Ported from the daslang-live host's C++ collector. Needs `options gc` + `options persistent_heap`. Expose the reserved-capacity counters the collector needs: heap_total_allocated() / string_heap_total_allocated() (Context::heap/stringHeap->totalAlignedMemoryAllocated()), paired with the existing *_bytes_allocated() live-byte counters. Wire all five example games (arcanoid, asteroids, pacman, river_run, sequence) to `options gc` + maybe_collect_gc() so they reclaim memory when run standalone. Also clears the pre-existing lint warnings in every touched file (mostly redundant int() casts on already-int args). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves standalone daslang game.das execution by aligning timing and garbage-collection behavior with the daslang-live host, so per-frame dt is stable and heaps are periodically reclaimed.
Changes:
- Cache standalone
dtonce per frame viaadvance_frame_clock, makingget_dt()idempotent within a frame. - Add GC support for standalone loops (
maybe_collect_gc) and expose heap reserved-capacity counters (*_total_allocated). - Update example games to enable GC and invoke the new per-frame collection helper; includes assorted lint cleanups.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/builtin/module_builtin_runtime.cpp | Exposes heap_total_allocated / string_heap_total_allocated to report reserved heap capacity. |
| modules/dasLiveHost/src/dasLiveHost.h | Declares live_advance_frame_clock for AOT/exports. |
| modules/dasLiveHost/src/dasLiveHost.cpp | Implements cached per-frame dt in standalone mode and binds advance_frame_clock to daScript. |
| modules/dasGlfw/dasglfw/glfw_live.das | Calls advance_frame_clock() in live_begin_frame; adds maybe_collect_gc() helper for standalone loops; minor lint fixes. |
| examples/games/sequence/main.das | Enables options gc and calls maybe_collect_gc() in the standalone loop. |
| examples/games/river_run/main.das | Enables options gc and calls maybe_collect_gc() in the standalone loop; minor lint fixes. |
| examples/games/pacman/main.das | Enables options gc and calls maybe_collect_gc() in the standalone loop; minor lint fixes + nolint rationale comments. |
| examples/games/asteroids/main.das | Enables options gc and calls maybe_collect_gc() in the standalone loop; minor lint fixes + nolint rationale comments. |
| examples/games/arcanoid/main.das | Enables options gc and calls maybe_collect_gc() in the standalone loop; minor lint fixes + nolint rationale comments. |
| doc/source/stdlib/handmade/function-builtin-string_heap_total_allocated-0x7cb9ab66ff86158e.rst | Documents the new builtin function. |
| doc/source/stdlib/handmade/function-builtin-heap_total_allocated-0x83b34cf36450f448.rst | Documents the new builtin function. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… json require - maybe_collect_gc() now early-returns under the live host (is_live_mode()), matching its documented contract — the host already collects each frame, so the helper is now safe to call unconditionally. Mirrors the get_dt no-op. - Drop the redundant `require daslib/json` from arcanoid/asteroids/pacman: daslib/json_boost already re-exports it (`require daslib/json public`), so the direct require (and its nolint) was noise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t_builtin.h AOT-generated C++ emits bare `heap_total_allocated(__context__)` / `string_heap_total_allocated(__context__)` calls (verified via codegen), so they need DAS_API prototypes in the AOT header alongside the existing heap_bytes_allocated / string_heap_bytes_allocated — otherwise any AOT build that calls them fails to compile for lack of a prototype. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-frame cache made get_dt() depend on advance_frame_clock() being called, which silently broke the documented "get_dt computes time internally in non-live mode" contract for any direct live_host caller that doesn't drive frames through glfw_live (test_lifecycle.das asserted it only as >= 0, so it passed vacuously). Add a one-way frame_clock_driven latch: until a frame driver (live_begin_frame -> advance_frame_clock, or the host) owns the clock, get_dt() self-advances per call (legacy behavior); once driven it returns the stable per-frame cache (the standalone-loop fix). Driven callers get idempotency, undriven direct callers keep old behavior — no regression either way. Avoids the alternative self-advance-always fallback, which would reintroduce the multi-call frame-split this PR fixes. Add a deterministic regression test for the idempotency property. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…al_allocated Asserts the binding/ABI invariants: total reserved >= live bytes for both heaps, totals non-zero and non-decreasing across a large allocation. Placed in tests/gc without `options no_aot`, so it also exercises the AOT codegen path for these builtins (validating the aot_builtin.h prototypes end-to-end). Green across interp, JIT, and AOT. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ct_gc heap_collect's first arg adds the string-heap walk (Context::collectHeap: the value heap is always collected; sheap=true also walks the string heap). The value-heap branch is reached only after the string heap was found NOT mostly free, so collecting it there is wasted per-frame work. Use heap_collect(false, false) to collect just the value heap. The string-heap branch keeps (true) — that's where the string heap is the mostly-free one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…be_collect_gc" This reverts c14715e. The GC walk (Context::collectHeap traversing the object graph from roots) is shared and happens for the value heap either way; sheap=true just also marks the string objects already being visited plus a linear string mark/sweep. So when the value heap is being collected, reclaiming the string heap too is near-free — and it reclaims more. The micro-optimization traded real reclamation for a negligible saving. Keep heap_collect(true, false) in both branches; comment documents why. Co-Authored-By: Claude Opus 4.8 <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.
A standalone
daslang game.dasruns its owndef mainloop, where the daslang-live host's clock and garbage collection are absent. Two gaps surface only there:get_dt()was a per-call stopwatchIt recomputed the delta on every call, so calling it twice in one frame split that frame's delta across the two calls (the second sees ~0). Now the frame delta is computed once per frame in
live_begin_frame(live_advance_frame_clock) and cached;get_dt()returns that stable value for the rest of the frame. Under the live host this is a no-op — the host still owns the clock.the heap is never collected standalone
Added
maybe_collect_gc()toglfw_live. It collects a heap only when it is mostly free (cheap compaction — little live data to walk), so it is safe to call every frame after update/render. The string heap fills faster, so it gets the lower threshold and is checked first. Ported from the daslang-live host's C++ collector. Requiresoptions gc+options persistent_heap.To drive the collector's "how full is the heap?" decision, expose the reserved-capacity counters alongside the existing live-byte ones:
heap_total_allocated()/string_heap_total_allocated()— bytes the context heap / string heap have reserved from the OS (totalAlignedMemoryAllocated), including currently-free spaceheap_bytes_allocated()/string_heap_bytes_allocated()(live bytes in use)example games
All five example games (arcanoid, asteroids, pacman, river_run, sequence) now declare
options gcand callmaybe_collect_gc()in their standalone loop, so they reclaim memory instead of growing unbounded. Verified each runs standalone with the heap reclaiming (e.g. asteroids peak ~120 MB to ~17 MB live).lint
Also clears the pre-existing lint warnings across every touched file — mostly redundant
int(...)casts on already-intarguments (GLFW key/button constants, character literals), plus a few+= 1to++, guard merges, and unused-arg renames. Requires that were flagged as unused are load-bearing side-effect registrations and arenolint'd with reasons.Preflight (full tier) green: format, lint, cpp-syntax, dasgen, ci-das, docs (das2rst + sphinx latex/html + pdflatex), tests-cpp/interp/jit/aot, sequence smoke.
🤖 Generated with Claude Code