Skip to content

standalone game loop: GC support + per-frame get_dt#3245

Merged
borisbat merged 7 commits into
masterfrom
bbatkin/standalone-loop-gc
Jun 22, 2026
Merged

standalone game loop: GC support + per-frame get_dt#3245
borisbat merged 7 commits into
masterfrom
bbatkin/standalone-loop-gc

Conversation

@borisbat

Copy link
Copy Markdown
Collaborator

A standalone daslang game.das runs its own def main loop, where the daslang-live host's clock and garbage collection are absent. Two gaps surface only there:

get_dt() was a per-call stopwatch

It 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() to glfw_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. Requires options 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 space
  • pair with the existing heap_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 gc and call maybe_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-int arguments (GLFW key/button constants, character literals), plus a few += 1 to ++, guard merges, and unused-arg renames. Requires that were flagged as unused are load-bearing side-effect registrations and are nolint'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

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>
Copilot AI review requested due to automatic review settings June 21, 2026 22:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 dt once per frame via advance_frame_clock, making get_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.

Comment thread modules/dasGlfw/dasglfw/glfw_live.das
Comment thread examples/games/pacman/main.das Outdated
Comment thread examples/games/asteroids/main.das Outdated
… 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread src/builtin/module_builtin_runtime.cpp
…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread modules/dasLiveHost/src/dasLiveHost.cpp
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment thread src/builtin/module_builtin_runtime.cpp
…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread modules/dasGlfw/dasglfw/glfw_live.das
…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

@borisbat borisbat merged commit f7f5b80 into master Jun 22, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants