feat(render): #86 @ClearColor — Render phase auto-clears + auto-presents
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m18s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s

@ClearColor(0xRRGGBB) declares the framebuffer clear colour, so the engine
owns the per-frame clear and flip: the Render phase clears to the colour at
the top and presents after the handlers run. Games drop the repeated
Screen.clear(color)/Screen.show() boilerplate, and the colour is configured
declaratively (an annotation) rather than in the handler body. Opt-in and
backward-compatible: a program with no @ClearColor is byte-for-byte identical
(it clears/presents itself, or the light system owns the present).

Parser reads @ClearColor(int) into g_clear_color/g_has_clear_color;
emit_game_main emits rt_clear before and rt_present after the Render phase,
gated on the flag. Example clear_color (pixel-readback verified — an undrawn
pixel holds the clear colour, proving the engine cleared), docs page +
inventory entry. Full suite 108/0, goldens byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-02 07:10:44 +03:00
parent bccd26fb29
commit d9287d6b1d
8 changed files with 18108 additions and 17936 deletions

3
changes/clear-color.md Normal file
View file

@ -0,0 +1,3 @@
bump: minor
type: feat
Declarative Render clear + present (#86) — `@ClearColor(0xRRGGBB)` makes the engine own the per-frame clear and flip: at the top of the Render phase it clears the framebuffer to the declared colour, and after the Render handlers run it presents the frame, so a game's Render handler no longer repeats `Screen.clear(color)` / `Screen.show()` and the clear colour is configured *declaratively* rather than in the handler body. Opt-in and backward-compatible: a program with no `@ClearColor` is byte-for-byte identical (it clears/presents itself, or the light system owns the present). Example: `examples/library/clear_color.ludic`.

View file

@ -0,0 +1,23 @@
---
id: annot-clearcolor
name: "@ClearColor"
category: annotations
kind: annotation
tokens: "@ClearColor"
sig: "@ClearColor(0xRRGGBB) handler Draw phase Render { … }"
tip: Declare a clear colour so the Render phase auto-clears + auto-presents for you.
order: 62
---
<code>@ClearColor(N)</code> declares the framebuffer clear colour (a <code>0x00RRGGBB</code> integer) so the engine owns the per-frame clear and present: at the top of the Render phase it clears the framebuffer to <code>N</code>, and after all Render handlers run it presents the frame — so a game's Render handler carries no <code>Screen.clear</code> / <code>Screen.show</code> boilerplate, and the colour is configured declaratively rather than written in the handler body. It is opt-in: a program with no <code>@ClearColor</code> is byte-for-byte identical (it clears and presents itself, or the light system owns the present). Place it on the program's Render handler (or any declaration — it is program-wide).
```ludic
program ClearColorDemo {
property Tag { n: int = 0 }
model P { Tag }
@ClearColor(0x102030)
handler Draw phase Render {
Screen.fill_rectangle(10, 10, 4, 4, 0xffffff)
}
}
```

View file

@ -0,0 +1,25 @@
# clear_color.ludic — @ClearColor (#86): the Render phase auto-clears to a
# declaratively-configured colour at the top and auto-presents after the handlers
# run, so the game's Render handler carries NO Screen.clear / Screen.show. Verified
# by pixel readback after the frame loop: an undrawn pixel holds the clear colour
# (proving the engine cleared), and the drawn box is present.
#
# Driven headless by one stdin key ('q' quits after the frame renders).
# printf 'q' | bin/ludic examples/library/clear_color.ludic -> 1 1
program ClearColorDemo {
property Tag { n: int = 0 }
model P { Tag }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
# No Screen.clear, no Screen.show — the engine clears to @ClearColor and presents.
@ClearColor(0x102030)
handler Draw phase Render {
Screen.fill_rectangle(10, 10, 4, 4, 0xffffff)
}
@OnQuit handler Report {
print(bi(Screen.pixel(200, 200) == 0x102030)) # 1 — background is the clear colour
print(bi(Screen.pixel(11, 11) == 0xffffff)) # 1 — the drawn box is there
}
}

View file

@ -486,7 +486,18 @@ function emit_game_main() -> void {
emit_calls_for_phase("FixedUpdate")
emit_calls_for_phase("Update")
emit_calls_for_phase("LateUpdate")
# #86: @ClearColor(N) makes the Render phase auto-clear to N at the top and
# auto-present after the handlers run, so a game drops the clear/show boilerplate
# (the clear colour is declared, not written in the handler body). Opt-in — a game
# with no @ClearColor is byte-identical (it clears/shows itself, or the light
# system owns the present).
if g_has_clear_color and (find_fn("rt_clear") != null) {
emit(" call void @fn_rt_clear(i32 "); emit(itoa(g_clear_color)); emit(")\n")
}
emit_calls_for_phase("Render")
if g_has_clear_color and (find_fn("rt_present") != null) {
emit(" call void @fn_rt_present()\n")
}
let fcur = emit_bind("load i32, ptr @L_frame") # Time.frame(): count completed frames
let fnext = emit_bind(`add i32 {fcur}, 1`)
emit(" store i32 "); emit(fnext); emit(", ptr @L_frame\n")

View file

@ -443,6 +443,8 @@ var g_uses_tiled: bool = false # Tiled.* -> splice the Tiled map runtime (#69
var g_uses_reflect_io: bool = false # Reflect.serialize/apply -> splice the reflection serializer
var g_uses_esys: bool = false # an engine-owned system component (SpriteAnim/Motion/Light2D) is declared -> splice systems.ludic + force the reflection ABI
var g_uses_input: bool = false # a program used Input.bind/down/poll/… (action maps + record/replay) -> splice input.ludic
var g_has_clear_color: bool = false # @ClearColor(N) declared -> the Render phase auto-clears + auto-presents (#86)
var g_clear_color: int = 0 # the declared clear colour (0x00RRGGBB)
var g_uses_anim_rt: bool = false # Anim.play/clip/on_frame/fired or Motion.to (#48) -> splice systems.ludic + force the reflection ABI
var g_uses_tween_rt: bool = false # Tween.to/chain/delay/… (#48) -> splice tween.ludic + run esys_tween each Update
var g_uses_audio: bool = false # Audio.* (#22) -> splice audio.ludic; a windowed build also links audio.ll + AVFoundation
@ -542,8 +544,11 @@ function parse_one_decl() -> void {
else { if (a == "System") { sys_phase = "Update"; if is_op("(") { pi = pi + 1; sys_phase = eat_id(); eat_op(")") } } # @System(Phase) binary-module system (#64)
else { if (a == "EngineSystem") { eat_op("("); esys_comp = eat_id(); eat_op(","); esys_phase = eat_id(); eat_op(")") } # @EngineSystem(Comp, Phase) package engine system (#62)
else { if (a == "Namespace") { eat_op("("); ns_name = eat_id(); eat_op(")") } # @Namespace(Name) package Foo.* namespace (#62)
else { if (a == "ClearColor") { eat_op("(") # @ClearColor(N) — Render auto-clear + auto-present (#86)
if toks[pi].kind != TK_INT { perr("@ClearColor expects an integer colour, e.g. @ClearColor(0x101018)") }
g_clear_color = toks[pi].ival; g_has_clear_color = true; pi = pi + 1; eat_op(")") }
else { if is_op("(") { var d = 0 # any other @anno(args) — parsed and skipped
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } } } } } } } } } } } } } } } } } } } } } } }
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } } } } } } } } } } } } } } } } } } } } } } } }
skipnl()
}
if is_id("import") { pi = pi + 1
@ -875,6 +880,8 @@ function parse_program() -> void {
g_uses_reflect = false
g_uses_esys = false
g_uses_input = false
g_has_clear_color = false
g_clear_color = 0
g_uses_light = false
g_uses_value = false
g_uses_xml = false

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,8 @@
"annot-toclients",
"annot-system",
"annot-enginesystem",
"annot-namespace"
"annot-namespace",
"annot-clearcolor"
],
"builtins": [
"fn-print",

View file

@ -224,6 +224,7 @@ function cmd_test() -> int {
feat_case("library/anim", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34", "anim.ludic (Anim frame/once/pingpong/cell + Tween progress/loop/yoyo/ease/number/round/point/tint)")
feat_case("library/anim_sugar", "", "4 8 2 1 0 100 100 0 0 1 20 20 30 0 1", "anim_sugar.ludic (Anim.clip/play/on_frame/fired + Motion.to + fluent Tween.to/chain/delay/parallel handles; issue #48)")
feat_case("library/camera_zoom", "", "1 0 0 1 1", "camera_zoom.ludic (Camera.zoom deterministic Q16.16 render-time zoom about the screen centre, verified by pixel readback; issue #78)")
feat_case("library/clear_color", "q", "1 1", "clear_color.ludic (@ClearColor: the Render phase auto-clears to the declared colour + auto-presents, no Screen.clear/show in the handler; issue #86)")
feat_case("library/query", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "query.ludic (Query count/first/nearest/within — ECS spatial queries over the reflection ABI)")
feat_case("library/reflect", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20", "reflect.ludic (Reflect prop/field enumeration + type + get/set/has/kind — runtime reflection over the world schema)")
feat_case("library/serialize", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16", "serialize.ludic (Value tree + Json encode/parse + Reflect.serialize/apply — bit-exact save/load; issue #44)")