From 3137df4fe630f0bec88ea540f9a7b00aaf70234a Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Fri, 11 Sep 2026 15:43:16 +0300 Subject: [PATCH] fix(render3d): outline_model's batch survives the whole frame A frame is drawn by more than one pass - a shadow map, a water reflection, the scene - and actor_draw runs in each. An Actor's rim survives that because it is a field on the actor; the queue did not, because the first pass to run emptied it. A queued outline was drawn into whichever target came first and was gone by the time the scene was drawn, so nothing appeared with every uniform, matrix and mesh correct - which took a while to find. A flush now closes the batch rather than clearing it, and the next outline_model opens a new one. Co-Authored-By: Claude Opus 5 --- changes/outline-queue-lifetime.md | 12 ++++++++++++ packages/ludic.render3d/actor.ludic | 11 +++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 changes/outline-queue-lifetime.md diff --git a/changes/outline-queue-lifetime.md b/changes/outline-queue-lifetime.md new file mode 100644 index 00000000..6eebe4ea --- /dev/null +++ b/changes/outline-queue-lifetime.md @@ -0,0 +1,12 @@ +bump: patch +type: fix +**`outline_model`'s batch survives the whole frame.** A frame is drawn by more than one +pass — a shadow map, a water reflection, the scene — and `actor_draw` runs in each of +them. An Actor's rim survives that because it is a field on the actor; the queue did not, +because the first pass to run emptied it, so a queued outline was drawn into whichever +target happened to come first and was gone by the time the scene was drawn. Nothing +appeared, with every uniform, matrix and mesh correct. + +A flush now *closes* the batch rather than clearing it, and the next `outline_model` +opens a new one: every pass in a frame sees the same requests, and a caller still needs +no frame hook. diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index edcc70dc..55300c94 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -213,8 +213,15 @@ property OutlineReq { } var ac_oq: []OutlineReq = null var ac_oq_n: int = 0 # live entries; the array is kept and reused +# A frame is drawn by more than one pass - a shadow map, a water reflection, the scene - +# and actor_draw runs in each of them. An Actor survives that because its rim is a field +# on the actor; a queue does not, if the first pass empties it. So a flush CLOSES the +# batch rather than clearing it, and the next add opens a new one: every pass in a frame +# sees the same requests, and the caller needs no frame hook. +var ac_oq_closed: bool = true function outline_model(m: Model, mat: words, width: int, r: int, g: int, b: int) -> void { if m == null or mat == null or width == 0 { return } + if ac_oq_closed { ac_oq_n = 0; ac_oq_closed = false } if ac_oq == null { ac_oq = new []OutlineReq } var q: OutlineReq = null if ac_oq_n < len(ac_oq) { q = ac_oq[ac_oq_n] } else { q = new OutlineReq; q.mat = m4_new(); push(ac_oq, q) } @@ -222,7 +229,7 @@ function outline_model(m: Model, mat: words, width: int, r: int, g: int, b: int) m4_copy(q.mat, mat) ac_oq_n += 1 } -function outline_clear() -> void { ac_oq_n = 0 } +function outline_clear() -> void { ac_oq_n = 0; ac_oq_closed = true } function actor_draw() -> void { if ac_actors == null { return } @@ -271,7 +278,7 @@ function actor_draw_outlines() -> void { u_f(ap.l_skin, F_ZERO) for k in 0 .. len(q.model.prims) { mesh_draw(q.model.prims[k].mesh) } } - ac_oq_n = 0 + ac_oq_closed = true gl_cull_face(GL_BACK) } function actor_draw_outline_one(a: Actor, ap: AcProg) -> void {