From 625c116eb0d21f4f5700a9c526c5c6694e7c630e Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Tue, 1 Sep 2026 14:46:58 +0000 Subject: [PATCH 1/2] picogame: do not shift StripDraw/Triangles dirty rects by the scene view Both layer kinds are documented and composited in screen coordinates: the draw path never applies the view offset to them. The dirty-rect pass applied it anyway to every non-fixed layer, so with a scrolled view a non-fixed StripDraw's invalidate() repainted an unrelated region while the layer's actual rectangle kept its stale pixels. A non-fixed Triangles batch similarly missed the band above/left of the view offset. Repro (device): StripDraw(cb, 100, 60, 40, 30, always_dirty=False) + scene.set_view(50, 20) + sd.invalidate() -> refresh() returned (150, 80, 190, 110) and the layer at (100, 60) never repainted. --- shared-module/picogame/Scene.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shared-module/picogame/Scene.c b/shared-module/picogame/Scene.c index 0d9e31c6fbc..2e9a2b0933b 100644 --- a/shared-module/picogame/Scene.c +++ b/shared-module/picogame/Scene.c @@ -74,8 +74,13 @@ int picogame_scene_compute_dirty_rects( for (size_t i = 0; i < n; i++) { uint8_t rawk = kinds[i]; uint8_t kind = rawk & PICOGAME_KIND_MASK; - int iox = (rawk & PICOGAME_KIND_FIXED) ? 0 : ox; - int ioy = (rawk & PICOGAME_KIND_FIXED) ? 0 : oy; + // StripDraw and Triangles always composite in screen coordinates (the draw + // path never applies the view offset to them), so their dirty rects must not + // shift with the view either - otherwise a scrolled scene repaints an + // unrelated region and leaves the layer's real change stale on screen. + bool screen_space = kind == PICOGAME_KIND_STRIPDRAW || kind == PICOGAME_KIND_TRIANGLES; + int iox = ((rawk & PICOGAME_KIND_FIXED) || screen_space) ? 0 : ox; + int ioy = ((rawk & PICOGAME_KIND_FIXED) || screen_space) ? 0 : oy; if (kind != PICOGAME_KIND_SPRITE) { int tx1, ty1, tx2, ty2; bool d = false; From 5f5a197a700c2cb243813a3a4eb2bae9d45af29d Mon Sep 17 00:00:00 2001 From: Vladimir Smitka Date: Thu, 27 Aug 2026 20:50:55 +0000 Subject: [PATCH 2/2] picogame: clip dirty rectangles to the screen before storing them A change wholly off-screen (the off-screen half of a wrapping parallax pair, a sprite parked outside) took a dirty slot anyway, and a scrolling background of wide strips burned two slots per strip - pushing a busy scene into the full-repaint overflow every frame. Clip to the screen first: fully invisible changes take no slot, partly visible ones are stored at their visible size, so the merge pass works on what actually gets repainted. Measured on a PicoPad (2026-08): the artifact-repro scene went from full repaints every frame to staying inside the 6-slot budget. --- shared-module/picogame/Scene.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/shared-module/picogame/Scene.c b/shared-module/picogame/Scene.c index 2e9a2b0933b..b50a6c7a9b1 100644 --- a/shared-module/picogame/Scene.c +++ b/shared-module/picogame/Scene.c @@ -44,8 +44,28 @@ static inline long rect_area(const picogame_rect_t *r) { // longer inflate the repaint to the whole screen. // Out-of-line rect append for compute_dirty_rects below: the old macro expanded ~30 B // four times; a real (noinline) call keeps each site at argument setup only. +// The rect is CLIPPED to the screen first: a change wholly off-screen (the off-screen half of +// a wrapping parallax pair, a sprite parked outside) takes no slot at all - a scrolling +// background made of many wide strips would otherwise burn two slots per strip and push a +// busy scene into the full-repaint overflow every frame - and a partly visible one is +// stored at its visible size, so the merge pass works on what actually gets repainted. static __attribute__((noinline)) void add_rect(picogame_rect_t *raw, int *nr, bool *overflow, - int x1, int y1, int x2, int y2) { + int screen_w, int screen_h, int x1, int y1, int x2, int y2) { + if (x1 < 0) { + x1 = 0; + } + if (y1 < 0) { + y1 = 0; + } + if (x2 > screen_w) { + x2 = screen_w; + } + if (y2 > screen_h) { + y2 = screen_h; + } + if (x1 >= x2 || y1 >= y2) { + return; // nothing visible changed + } if (*nr < PICOGAME_RAW_RECTS) { raw[*nr].x1 = x1; raw[*nr].y1 = y1; @@ -69,7 +89,7 @@ int picogame_scene_compute_dirty_rects( // Rects are stored in SCREEN coords: non-fixed items get the view offset added // here (per item), fixed (HUD) items don't - so no uniform offset at the end. #define ADD_RECT(ax, ay, bx, by) \ - add_rect(raw, &nr, &overflow, (ax) + iox, (ay) + ioy, (bx) + iox, (by) + ioy) + add_rect(raw, &nr, &overflow, screen_w, screen_h, (ax) + iox, (ay) + ioy, (bx) + iox, (by) + ioy) for (size_t i = 0; i < n; i++) { uint8_t rawk = kinds[i];