Skip to content

Commit e8df55e

Browse files
committed
fix(render): persist all one-shot BG bakes via accumulating layer
The korlab5 airlock door opened with the card reverted to closed the instant the open animation finished — still passable, and re-carding played the close animation, so only the visual was wrong. The open door bakes itself into the scene BG as a flag-0x60 one-shot overlay (drzwiotN.wyc, ~165-183px wide). SaveSceneBgAtlas kept a SINGLE atlas slot gated on width >= 320px (built only for the stub-pic full-background case), so the narrow door bake was never saved and the per-frame .pic repaint wiped it the next frame. Re-entry happened to work only because its drzwiot3 frame is >= 320px. The original engine (FUN_00411730) accumulates every such bake into one persistent BG page with no size gate. Mirror that: replace the single slot + width gate with a screen-sized bake layer (pixels + 1-byte coverage mask + dirty bbox). SaveSceneBgAtlas accumulates clean atlas pixels (overwrite-in-place, so open/close toggles show the latest state), PaintSceneBgAtlasIfAny overlays the masked bbox each frame, and paint_oneshot_bg bakes unconditionally. The HUD panel is drawn separately by hud_paint and never used this path, so dropping the gate is safe. Fixes narrow and multiple scene-prop bakes generally (the door plus the card-reader guziki/pasek). Verified: clean build, 481/481 unit tests, headless ASan/UBSan clean.
1 parent 38ce010 commit e8df55e

3 files changed

Lines changed: 96 additions & 55 deletions

File tree

include/wacki/globals.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ extern uint8_t g_palette_rgb[256*3];
2626
extern uint16_t g_screen_w, g_screen_h;
2727
extern uint8_t *g_back_shadow; /* 320×240×8bpp paletted shadow */
2828
extern uint16_t g_screen_w_dim, g_screen_h_dim;
29-
extern uint8_t *g_scene_bg_atlas_copy;
3029

3130
/* ---- script register file + scene state -------------------------- */
3231

src/actor/render.c

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,6 @@ extern Entity *g_actor[2];
5757
#define SCENE_BG_WIDTH_OFFSET 4
5858
#define SCENE_BG_HEIGHT_OFFSET 6
5959

60-
/* ---- one-shot BG persistence heuristic ----------------------------- */
61-
/* HUD panel atlases (small button strips at the bottom of the screen)
62-
* also use the one-shot blit path, but we must NOT cache them as the
63-
* persistent scene background. Heuristic: only cache when the sprite
64-
* spans at least half the play-area width AND its top is high enough
65-
* to suggest it covers the upper part of the screen. */
66-
#define BG_PERSIST_MIN_WIDTH 320
67-
#define BG_PERSIST_TOP_LIMIT 200
68-
#define BG_PERSIST_BOT_LIMIT 380
6960

7061
/* ---- viewport off-screen guard (generous on every edge so mid-
7162
* flight off-screen sprites still render their tail). The left/top
@@ -145,14 +136,13 @@ static void stamp_paint_mask(int x0, int y0, int w, int h)
145136

146137
/* ---- one-shot BG sprite (flag 0x40 | 0x20) -------------------------- *
147138
*
148-
* Some komnaty ship a 1×1 stub .pic and rely on a regular entity
149-
* spawn to paint the actual background — this fires once on the first
150-
* tick after the entity is spawned, then clears its own pending bit so
151-
* subsequent frames don't repaint. We also stash the atlas pixels via
152-
* SaveSceneBgAtlas so the per-frame BG repaint can find them.
153-
*
154-
* The size heuristic prevents HUD panel strips (which also use the
155-
* flag-0x40|0x20 path) from being cached as the scene BG. */
139+
* Fires once after a flag-0x60 entity (or a per-entity fade bake) reaches
140+
* a frame: paints the current frame to the backbuffer and clears the
141+
* pending bit so it doesn't repaint. The pixels are also handed to
142+
* SaveSceneBgAtlas, which accumulates them into the persistent bake layer
143+
* so the per-frame BG repaint re-applies them — that's how a stub .pic's
144+
* real background, the airlock door, and the card-reader props all stay
145+
* painted after their source entity is destroyed. */
156146
static void paint_oneshot_bg(Entity *e, uint16_t flags, AnimAsset *atlas)
157147
{
158148
if (!atlas || !atlas->frame_count || !atlas->pixel_ptrs) return;
@@ -180,11 +170,12 @@ static void paint_oneshot_bg(Entity *e, uint16_t flags, AnimAsset *atlas)
180170
* advanced its animation frame. The outer FlushFrameToPrimary at
181171
* the end of paint_frame already presents the complete buffer. */
182172

183-
int top_high_enough = (by < BG_PERSIST_TOP_LIMIT) ||
184-
((by + fh) >= BG_PERSIST_BOT_LIMIT);
185-
if (fw >= BG_PERSIST_MIN_WIDTH && top_high_enough) {
186-
SaveSceneBgAtlas(bx, by, fw, fh, px);
187-
}
173+
/* Accumulate into the persistent bake layer so the overlay survives
174+
* the per-frame .pic repaint. Unconditional, matching the original's
175+
* un-gated BG-page accumulation: the layer holds every bake (stub-pic
176+
* background, narrow scene props like the airlock door), so there's no
177+
* single slot to protect and no size gate to drop small props. */
178+
SaveSceneBgAtlas(bx, by, fw, fh, px);
188179
}
189180

190181
/* ---- walk-behind mask: 1bpp shape → BG-through-mask blit ------------ *

src/graphics.c

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,50 +29,101 @@ uint16_t g_screen_h = WACKI_SCREEN_H;
2929
uint16_t g_screen_w_dim = WACKI_SCREEN_W;
3030
uint16_t g_screen_h_dim = WACKI_SCREEN_H;
3131

32-
/* Scene-BG atlas copy — stub-pic komnaty (e.g. magaz3j where the table
33-
* .pic is a 1×1 palette placeholder) spawn their real BG as a kind=2
34-
* atlas entity with flag-0x60 in their enter_va. The one-shot blit
35-
* paths it to the backbuffer ONCE, then second_va destroys the entity
36-
* — freeing the atlas. So we copy the atlas frame's pixels here on the
37-
* one-shot path and own the copy ourselves; per-frame paint repaints
38-
* the BG image (not a backbuffer snapshot — earlier impl snapshot
39-
* captured whatever else was on backbuffer at the moment, including
40-
* leftover sprites from the prior komnata → "static silhouettes"
41-
* behind every moving thing). Freed on komnata transition. */
42-
uint8_t *g_scene_bg_atlas_copy = NULL;
43-
uint16_t g_scene_bg_atlas_w = 0;
44-
uint16_t g_scene_bg_atlas_h = 0;
45-
int16_t g_scene_bg_atlas_dx = 0;
46-
int16_t g_scene_bg_atlas_dy = 0;
32+
/* ---- persistent one-shot-BG bake layer ----------------------------- *
33+
*
34+
* Entities flagged EFLAG_FADE_OR_BG | EFLAG_ONESHOT_BG_PEND (flag-0x60
35+
* spawns, plus per-entity fade bakes) paint their current frame into the
36+
* scene background ONCE, then the source entity is usually destroyed. The
37+
* original engine accumulates every such bake into one persistent BG page
38+
* (FUN_00411730) and repaints from it, so any number of bakes coexist
39+
* permanently — a stub .pic's full background, the korlab5 airlock door,
40+
* the card-reader buttons.
41+
*
42+
* We mirror that with a screen-sized pixel buffer + 1-byte coverage mask.
43+
* SaveSceneBgAtlas copies the (clean) atlas pixels in and marks the rect;
44+
* PaintSceneBgAtlasIfAny overlays the marked pixels onto the
45+
* freshly-painted .pic every frame. Storing clean atlas pixels (never a
46+
* backbuffer snapshot) avoids the "static silhouette" artefact a snapshot
47+
* baked in. A re-bake over the same area overwrites in place, so
48+
* open→close→open toggles always show the latest state. A dirty bbox
49+
* keeps the per-frame overlay cheap. Freed on komnata transition.
50+
*
51+
* NOTE: replaces an earlier single-atlas slot that held only ONE bake and
52+
* gated saves on width ≥ 320 px — which dropped narrow scene props, most
53+
* visibly the korlab5 airlock door (~165 px), so it reverted to the
54+
* .pic's closed state the instant the per-frame repaint ran. The width
55+
* gate only existed to stop a small prop from evicting the real BG out of
56+
* that single slot; the layer makes both the gate and the slot moot,
57+
* matching the original's un-gated accumulation. */
58+
static uint8_t *s_bg_bake_px = NULL; /* screen-sized accumulated pixels */
59+
static uint8_t *s_bg_bake_mask = NULL; /* 1 where a bake covers the pixel */
60+
static int s_bg_bake_x0, s_bg_bake_y0; /* dirty bbox of baked area; */
61+
static int s_bg_bake_x1, s_bg_bake_y1; /* x1<=x0 ⇒ nothing baked yet */
62+
63+
static int ensure_bg_bake_layer(void)
64+
{
65+
size_t n = (size_t)g_screen_w * g_screen_h;
66+
if (!s_bg_bake_px) s_bg_bake_px = (uint8_t *)xmalloc((uint32_t)n);
67+
if (!s_bg_bake_mask) {
68+
s_bg_bake_mask = (uint8_t *)xmalloc((uint32_t)n);
69+
if (s_bg_bake_mask) memset(s_bg_bake_mask, 0, n);
70+
}
71+
return s_bg_bake_px && s_bg_bake_mask;
72+
}
4773

4874
void SaveSceneBgAtlas(int16_t dx, int16_t dy,
4975
uint16_t w, uint16_t h, const uint8_t *src)
5076
{
51-
if (!src || !w || !h) return;
52-
size_t n = (size_t)w * h;
53-
if (g_scene_bg_atlas_copy) { xfree(g_scene_bg_atlas_copy); g_scene_bg_atlas_copy = NULL; }
54-
g_scene_bg_atlas_copy = (uint8_t *)xmalloc((uint32_t)n);
55-
if (!g_scene_bg_atlas_copy) return;
56-
memcpy(g_scene_bg_atlas_copy, src, n);
57-
g_scene_bg_atlas_w = w;
58-
g_scene_bg_atlas_h = h;
59-
g_scene_bg_atlas_dx = dx;
60-
g_scene_bg_atlas_dy = dy;
77+
if (!src || !w || !h || !ensure_bg_bake_layer()) return;
78+
79+
/* Clip (dx,dy,w,h) to the screen, tracking the source offset so a
80+
* partially off-screen bake still copies the right pixels. */
81+
int x0 = dx, y0 = dy, x1 = dx + (int)w, y1 = dy + (int)h;
82+
int sx = 0, sy = 0;
83+
if (x0 < 0) { sx = -x0; x0 = 0; }
84+
if (y0 < 0) { sy = -y0; y0 = 0; }
85+
if (x1 > (int)g_screen_w) x1 = (int)g_screen_w;
86+
if (y1 > (int)g_screen_h) y1 = (int)g_screen_h;
87+
if (x0 >= x1 || y0 >= y1) return;
88+
89+
for (int y = y0; y < y1; ++y) {
90+
const uint8_t *s = src + (size_t)(sy + (y - y0)) * w + sx;
91+
size_t off = (size_t)y * g_screen_w + x0;
92+
memcpy(s_bg_bake_px + off, s, (size_t)(x1 - x0));
93+
memset(s_bg_bake_mask + off, 1, (size_t)(x1 - x0));
94+
}
95+
96+
if (s_bg_bake_x1 <= s_bg_bake_x0) { /* first bake this scene */
97+
s_bg_bake_x0 = x0; s_bg_bake_y0 = y0;
98+
s_bg_bake_x1 = x1; s_bg_bake_y1 = y1;
99+
} else {
100+
if (x0 < s_bg_bake_x0) s_bg_bake_x0 = x0;
101+
if (y0 < s_bg_bake_y0) s_bg_bake_y0 = y0;
102+
if (x1 > s_bg_bake_x1) s_bg_bake_x1 = x1;
103+
if (y1 > s_bg_bake_y1) s_bg_bake_y1 = y1;
104+
}
61105
}
62106

63107
void PaintSceneBgAtlasIfAny(void)
64108
{
65-
if (!g_scene_bg_atlas_copy) return;
66-
PaintImageToBackbuffer(g_scene_bg_atlas_dx, g_scene_bg_atlas_dy,
67-
g_scene_bg_atlas_w, g_scene_bg_atlas_h,
68-
g_scene_bg_atlas_copy);
109+
if (!s_bg_bake_px || !s_bg_bake_mask || !g_back_shadow) return;
110+
if (s_bg_bake_x1 <= s_bg_bake_x0) return; /* nothing baked yet */
111+
112+
for (int y = s_bg_bake_y0; y < s_bg_bake_y1; ++y) {
113+
size_t row = (size_t)y * g_screen_w;
114+
const uint8_t *mk = s_bg_bake_mask + row;
115+
const uint8_t *px = s_bg_bake_px + row;
116+
uint8_t *bb = g_back_shadow + row;
117+
for (int x = s_bg_bake_x0; x < s_bg_bake_x1; ++x)
118+
if (mk[x]) bb[x] = px[x];
119+
}
69120
}
70121

71122
void FreeSceneBgAtlas(void)
72123
{
73-
if (g_scene_bg_atlas_copy) { xfree(g_scene_bg_atlas_copy); g_scene_bg_atlas_copy = NULL; }
74-
g_scene_bg_atlas_w = g_scene_bg_atlas_h = 0;
75-
g_scene_bg_atlas_dx = g_scene_bg_atlas_dy = 0;
124+
if (s_bg_bake_px) { xfree(s_bg_bake_px); s_bg_bake_px = NULL; }
125+
if (s_bg_bake_mask) { xfree(s_bg_bake_mask); s_bg_bake_mask = NULL; }
126+
s_bg_bake_x0 = s_bg_bake_y0 = s_bg_bake_x1 = s_bg_bake_y1 = 0;
76127
}
77128

78129
/* ---- dirty-rect tracking (kept for fidelity, not used by SDL preset) --- */

0 commit comments

Comments
 (0)