feat(assets): #82 incremental asset preloading + loading-scene pattern
Assets loaded synchronously in Boot stalled the first frame(s). Adds an Assets.* preload queue over the #81 atlas: Assets.enqueue(name, path) queues a named image without loading it, Assets.pump(max) loads up to max per frame (returns how many), and Assets.total/loaded/ready/progress (0..100) drive a progress bar. A loading scene pumps a few per frame, draws Assets.progress(), and becomes the play scene once Assets.ready() — the deterministic, no-threads form of async preloading (work spread across frames; same enqueue+pump order loads identically every run). Loaded assets are reachable by name via Assets.get / Sprite.named. Example preload (enqueue 3, pump incrementally 0->33->66->100, ready flips, get by name) prints 3 0 0 0 1 33 66 1 100 1. 6 docs pages. Full suite 117/0, fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
23e232e380
commit
f2cb3cd7e8
12 changed files with 22197 additions and 21863 deletions
3
changes/asset-preload.md
Normal file
3
changes/asset-preload.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
Incremental asset preloading (#82). Assets used to load synchronously inside Boot/Start (`png_load`, `Audio.load`), stalling the first frame(s) as content grows, with no built-in loading phase. Adds an `Assets.*` preload queue: `Assets.enqueue(name, path)` queues a named image without loading it, `Assets.pump(max)` loads up to `max` queued assets per frame (returning how many it loaded), and `Assets.total` / `loaded` / `ready` / `progress` (0..100) drive a progress bar. A loading scene pumps a few assets per frame, draws `Assets.progress()`, and `become`s the play scene once `Assets.ready()`, so the game shows a responsive loading screen and only enters play once content is ready — the deterministic, no-threads form of async preloading (the work is spread across frames instead of stalling one, and the same enqueue+pump order loads identically every run). Loaded assets are reachable by name via `Assets.get` / `Sprite.named`. Builds on the #81 atlas. Example: `examples/library/preload.ludic`.
|
||||||
14
docs/language/assets/assets-enqueue.md
Normal file
14
docs/language/assets/assets-enqueue.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: assets-enqueue
|
||||||
|
name: Assets.enqueue
|
||||||
|
category: assets
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Assets.enqueue
|
||||||
|
sig: Assets.enqueue(name, path)
|
||||||
|
tip: Queue a named image file to preload later.
|
||||||
|
order: 4
|
||||||
|
ns: Assets
|
||||||
|
member: enqueue
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds a named image file to the preload queue without loading it now. Load the queue incrementally with <a href="assets-pump"><code>Assets.pump</code></a> from a loading scene, then reach each asset by name with <a href="assets-get"><code>Assets.get</code></a> once <a href="assets-ready"><code>Assets.ready</code></a>. Enqueueing is deterministic — the same order loads the same assets in the same order every run.
|
||||||
14
docs/language/assets/assets-loaded.md
Normal file
14
docs/language/assets/assets-loaded.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: assets-loaded
|
||||||
|
name: Assets.loaded
|
||||||
|
category: assets
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Assets.loaded
|
||||||
|
sig: Assets.loaded() -> int
|
||||||
|
tip: How many enqueued assets have loaded so far.
|
||||||
|
order: 7
|
||||||
|
ns: Assets
|
||||||
|
member: loaded
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns how many of the enqueued assets have loaded so far (advanced by <a href="assets-pump"><code>Assets.pump</code></a>) — the numerator for a progress bar.
|
||||||
14
docs/language/assets/assets-progress.md
Normal file
14
docs/language/assets/assets-progress.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: assets-progress
|
||||||
|
name: Assets.progress
|
||||||
|
category: assets
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Assets.progress
|
||||||
|
sig: Assets.progress() -> int
|
||||||
|
tip: Loading progress as a 0..100 percent.
|
||||||
|
order: 9
|
||||||
|
ns: Assets
|
||||||
|
member: progress
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the loading progress as a whole-number percent (<code>0..100</code>); an empty queue is <code>100</code>. Draw it as a progress bar on the loading screen.
|
||||||
14
docs/language/assets/assets-pump.md
Normal file
14
docs/language/assets/assets-pump.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: assets-pump
|
||||||
|
name: Assets.pump
|
||||||
|
category: assets
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Assets.pump
|
||||||
|
sig: Assets.pump(max) -> int
|
||||||
|
tip: Load up to max queued assets this frame; returns how many it loaded.
|
||||||
|
order: 5
|
||||||
|
ns: Assets
|
||||||
|
member: pump
|
||||||
|
---
|
||||||
|
|
||||||
|
Loads up to <code>max</code> queued assets this frame, registering each under its name, and returns how many it loaded. Call it each frame in a loading scene with a small <code>max</code> so the frame stays short and the loading screen animates; <a href="assets-ready"><code>Assets.ready</code></a> flips true when the queue is drained. This is the deterministic, no-threads form of async preloading — the work is spread across frames instead of stalling one.
|
||||||
14
docs/language/assets/assets-ready.md
Normal file
14
docs/language/assets/assets-ready.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: assets-ready
|
||||||
|
name: Assets.ready
|
||||||
|
category: assets
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Assets.ready
|
||||||
|
sig: Assets.ready() -> int
|
||||||
|
tip: 1 once every enqueued asset has loaded.
|
||||||
|
order: 8
|
||||||
|
ns: Assets
|
||||||
|
member: ready
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>1</code> once every enqueued asset has been loaded (the queue is drained), else <code>0</code>. A loading scene pumps until this is true, then enters play (e.g. <code>become</code> the game scene).
|
||||||
14
docs/language/assets/assets-total.md
Normal file
14
docs/language/assets/assets-total.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: assets-total
|
||||||
|
name: Assets.total
|
||||||
|
category: assets
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Assets.total
|
||||||
|
sig: Assets.total() -> int
|
||||||
|
tip: How many assets are enqueued.
|
||||||
|
order: 6
|
||||||
|
ns: Assets
|
||||||
|
member: total
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the total number of assets enqueued with <a href="assets-enqueue"><code>Assets.enqueue</code></a> — the denominator for a progress bar.
|
||||||
38
examples/library/preload.ludic
Normal file
38
examples/library/preload.ludic
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# preload.ludic — incremental asset preloading (#82). Instead of loading every
|
||||||
|
# asset synchronously in Boot (stalling the first frame), enqueue named files and
|
||||||
|
# load a bounded number per frame with Assets.pump, showing progress until
|
||||||
|
# Assets.ready(). Deterministic (no threads): the work is spread across frames.
|
||||||
|
#
|
||||||
|
# The loading-scene pattern: in a `scene Loading` a Render handler pumps a few
|
||||||
|
# assets, draws Assets.progress() as a bar, and `become`s the play scene once
|
||||||
|
# Assets.ready(). Here the API is driven from `entry` for a deterministic check.
|
||||||
|
#
|
||||||
|
# Deterministic; a full run prints: 3 0 0 0 1 33 66 1 100 1
|
||||||
|
program Preload {
|
||||||
|
# a model so the program runs the ECS and links the core runtime / framebuffer.
|
||||||
|
property Cam { z: fixed = 1.0 }
|
||||||
|
model V { Cam }
|
||||||
|
|
||||||
|
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||||
|
|
||||||
|
entry {
|
||||||
|
Assets.enqueue("hero", "assets/kenney/tiny-dungeon/Tiles/tile_0096.png")
|
||||||
|
Assets.enqueue("wall", "assets/kenney/tiny-dungeon/Tiles/tile_0036.png")
|
||||||
|
Assets.enqueue("door", "assets/kenney/tiny-dungeon/Tiles/tile_0037.png")
|
||||||
|
print(Assets.total()) # 3
|
||||||
|
print(Assets.loaded()) # 0
|
||||||
|
print(Assets.progress()) # 0
|
||||||
|
print(bi(Assets.ready())) # 0
|
||||||
|
|
||||||
|
Assets.pump(1) # load one this frame
|
||||||
|
print(Assets.loaded()) # 1
|
||||||
|
print(Assets.progress()) # 33
|
||||||
|
Assets.pump(1) # and one the next
|
||||||
|
print(Assets.progress()) # 66
|
||||||
|
Assets.pump(5) # drain the rest
|
||||||
|
print(bi(Assets.ready())) # 1
|
||||||
|
print(Assets.progress()) # 100
|
||||||
|
|
||||||
|
print(bi(Assets.get("hero") >= 0)) # 1 — reachable by name once loaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,16 @@ var at_name_str: pointers = null # name per named sprite
|
||||||
var at_name_id: words = null # atlas id per name
|
var at_name_id: words = null # atlas id per name
|
||||||
var at_nname: int = 0
|
var at_nname: int = 0
|
||||||
|
|
||||||
|
# #82 — an incremental preload queue: enqueue named image files, then load a
|
||||||
|
# bounded number per frame (Assets.pump) so a loading scene stays responsive and
|
||||||
|
# a game only enters play once Assets.ready(). Deterministic: the same enqueue +
|
||||||
|
# pump order loads the same assets in the same order on every run.
|
||||||
|
const ATLAS_MAX_QUEUE: int = 512
|
||||||
|
var at_q_name: pointers = null # name to register the loaded sprite under
|
||||||
|
var at_q_path: pointers = null # file path to load
|
||||||
|
var at_q_n: int = 0 # how many enqueued
|
||||||
|
var at_q_pos: int = 0 # how many loaded so far
|
||||||
|
|
||||||
function atlas_init() -> void {
|
function atlas_init() -> void {
|
||||||
if at_sheet_img != null { return }
|
if at_sheet_img != null { return }
|
||||||
at_sheet_img = words(ATLAS_MAX_SHEET)
|
at_sheet_img = words(ATLAS_MAX_SHEET)
|
||||||
|
|
@ -55,6 +65,17 @@ function atlas_init() -> void {
|
||||||
at_spr_h = words(ATLAS_MAX_SPR)
|
at_spr_h = words(ATLAS_MAX_SPR)
|
||||||
at_name_str = bytes(ATLAS_MAX_NAME * 8) # a pointer (8 bytes) per name slot
|
at_name_str = bytes(ATLAS_MAX_NAME * 8) # a pointer (8 bytes) per name slot
|
||||||
at_name_id = words(ATLAS_MAX_NAME)
|
at_name_id = words(ATLAS_MAX_NAME)
|
||||||
|
at_q_name = bytes(ATLAS_MAX_QUEUE * 8)
|
||||||
|
at_q_path = bytes(ATLAS_MAX_QUEUE * 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
# register (name -> atlas id) in the name table so Assets.get / Sprite.named find it.
|
||||||
|
function atlas_register_name(name: pointer, id: int) -> void {
|
||||||
|
atlas_init()
|
||||||
|
if at_nname >= ATLAS_MAX_NAME { return }
|
||||||
|
at_name_str[at_nname] = name
|
||||||
|
at_name_id[at_nname] = id
|
||||||
|
at_nname = at_nname + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# load a spritesheet PNG whose cells are cw x ch px; returns a sheet handle (>= 0).
|
# load a spritesheet PNG whose cells are cw x ch px; returns a sheet handle (>= 0).
|
||||||
|
|
@ -210,3 +231,39 @@ function atlas_draw_scaled(id: int, dx: int, dy: int, sc: int) -> void {
|
||||||
# an atlas sprite's pixel size — handy for centering / layout.
|
# an atlas sprite's pixel size — handy for centering / layout.
|
||||||
function atlas_width(id: int) -> int { atlas_init(); if (id < 0) or (id >= at_nspr) { return 0 }; return at_spr_w[id] }
|
function atlas_width(id: int) -> int { atlas_init(); if (id < 0) or (id >= at_nspr) { return 0 }; return at_spr_w[id] }
|
||||||
function atlas_height(id: int) -> int { atlas_init(); if (id < 0) or (id >= at_nspr) { return 0 }; return at_spr_h[id] }
|
function atlas_height(id: int) -> int { atlas_init(); if (id < 0) or (id >= at_nspr) { return 0 }; return at_spr_h[id] }
|
||||||
|
|
||||||
|
# ---- incremental preload (#82) --------------------------------------------
|
||||||
|
# Enqueue a named image file to load later (does not load it now).
|
||||||
|
function assets_enqueue(name: pointer, path: pointer) -> void {
|
||||||
|
atlas_init()
|
||||||
|
if at_q_n >= ATLAS_MAX_QUEUE { return }
|
||||||
|
at_q_name[at_q_n] = name
|
||||||
|
at_q_path[at_q_n] = path
|
||||||
|
at_q_n = at_q_n + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load up to `max` queued assets this frame, registering each under its name, and
|
||||||
|
# return how many were loaded by this call. Call it each frame in a loading scene
|
||||||
|
# (a small `max` keeps the frame short); Assets.ready() flips true when done.
|
||||||
|
function assets_pump(max: int) -> int {
|
||||||
|
atlas_init()
|
||||||
|
var done = 0
|
||||||
|
while (done < max) and (at_q_pos < at_q_n) {
|
||||||
|
let id = atlas_image(at_q_path[at_q_pos])
|
||||||
|
atlas_register_name(at_q_name[at_q_pos], id)
|
||||||
|
at_q_pos = at_q_pos + 1
|
||||||
|
done = done + 1
|
||||||
|
}
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
|
||||||
|
function assets_total() -> int { atlas_init(); return at_q_n }
|
||||||
|
function assets_loaded() -> int { atlas_init(); return at_q_pos }
|
||||||
|
function assets_ready() -> int { atlas_init(); if at_q_pos >= at_q_n { return 1 }; return 0 }
|
||||||
|
|
||||||
|
# loading progress as a whole-number percent (0..100); an empty queue is 100.
|
||||||
|
function assets_progress() -> int {
|
||||||
|
atlas_init()
|
||||||
|
if at_q_n <= 0 { return 100 }
|
||||||
|
return at_q_pos * 100 / at_q_n
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,13 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
||||||
if (meth == "image") { bare = "atlas_image"; push(labels, "path") }
|
if (meth == "image") { bare = "atlas_image"; push(labels, "path") }
|
||||||
if (meth == "load") { bare = "atlas_image"; push(labels, "path") } # alias
|
if (meth == "load") { bare = "atlas_image"; push(labels, "path") } # alias
|
||||||
if (meth == "get") { bare = "atlas_named"; push(labels, "name") } # alias of Sprite.named
|
if (meth == "get") { bare = "atlas_named"; push(labels, "name") } # alias of Sprite.named
|
||||||
|
# #82 — incremental preload queue + progress for a loading scene.
|
||||||
|
if (meth == "enqueue") { bare = "assets_enqueue"; push(labels, "name"); push(labels, "path") }
|
||||||
|
if (meth == "pump") { bare = "assets_pump"; push(labels, "max") }
|
||||||
|
if (meth == "total") { bare = "assets_total" }
|
||||||
|
if (meth == "loaded") { bare = "assets_loaded" }
|
||||||
|
if (meth == "ready") { bare = "assets_ready" }
|
||||||
|
if (meth == "progress") { bare = "assets_progress" }
|
||||||
}
|
}
|
||||||
# Camera.* — the world-space camera: a draw offset threaded through the render
|
# Camera.* — the world-space camera: a draw offset threaded through the render
|
||||||
# path (runtime/native/core.ludic). set/follow move it; shake jitters it from
|
# path (runtime/native/core.ludic). set/follow move it; shake jitters it from
|
||||||
|
|
|
||||||
43870
selfhost/ludicc.seed.ll
43870
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -242,6 +242,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", "", "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/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/namespace_block", "", "15 42 100", "namespace_block.ludic (#76 namespace Name { export/internal function } block — declares the namespace once, controls the public surface)")
|
feat_case("library/namespace_block", "", "15 42 100", "namespace_block.ludic (#76 namespace Name { export/internal function } block — declares the namespace once, controls the public surface)")
|
||||||
|
feat_case("library/preload", "", "3 0 0 0 1 33 66 1 100 1", "preload.ludic (#82 Assets.enqueue/pump/progress/ready — incremental asset preload for a loading scene)")
|
||||||
feat_case("library/atlas", "", "16 16 32 48 1 1 1 16 1", "atlas.ludic (#81 Sprite.sheet/cell/cell_span/define/named + Assets.image/get — namespaced spritesheet/atlas with multi-cell sprites)")
|
feat_case("library/atlas", "", "16 16 32 48 1 1 1 16 1", "atlas.ludic (#81 Sprite.sheet/cell/cell_span/define/named + Assets.image/get — namespaced spritesheet/atlas with multi-cell sprites)")
|
||||||
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/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/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)")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue