1212#include "shared-bindings/picogame/Tilemap.h"
1313#include "shared-bindings/picogame/Particles.h"
1414#include "shared-bindings/picogame/Canvas.h"
15+ #include "shared-bindings/picogame/Framebuffer.h"
1516#include "shared-bindings/busdisplay/BusDisplay.h"
1617#if CIRCUITPY_PICOGAME_FAST_DISPLAY
1718#include "shared-bindings/picogame/Display.h"
2829#define SCENE_INIT_CAP 8
2930#define PICOGAME_MAX_DIRTY_RECTS 6 // separate regions repainted per refresh
3031
32+ // Classify the `display` arg into a render backend and return the object Scene should
33+ // store: a fast picogame.Display (DMA), a picogame.Framebuffer (RAM scanout buffer, when
34+ // built in), or a plain busdisplay (portable bus.send). Sets *fast / *fb_target
35+ // accordingly; for a busdisplay it accepts a SUBCLASS (e.g. adafruit_st7789.ST7789) by
36+ // casting to its native base and returns that. Raises TypeError otherwise. This is where
37+ // the flag-gated type checks live, so the constructor body stays clean.
38+ static mp_obj_t scene_resolve_target (mp_obj_t disp , bool * fast , bool * fb_target ) {
39+ * fast = false;
40+ * fb_target = false;
41+ #if CIRCUITPY_PICOGAME_FAST_DISPLAY
42+ if (mp_obj_is_type (disp , & picogame_display_type )) {
43+ * fast = true;
44+ return disp ;
45+ }
46+ #endif
47+ #if CIRCUITPY_PICOGAME_FRAMEBUFFER
48+ if (mp_obj_is_type (disp , & picogame_framebuffer_type )) {
49+ * fb_target = true; // refresh() composites dirty rects straight into its RAM buffer
50+ return disp ;
51+ }
52+ #endif
53+ // Plain busdisplay: accept a subclass by casting to its native base; the portable
54+ // renderer treats self->display as a busdisplay_busdisplay_obj_t directly.
55+ mp_obj_t native = mp_obj_cast_to_native_base (disp , & busdisplay_busdisplay_type );
56+ if (!mp_obj_is_type (native , & busdisplay_busdisplay_type )) {
57+ mp_raise_TypeError (MP_ERROR_TEXT ("expected a BusDisplay" ));
58+ }
59+ return native ;
60+ }
61+
3162//| class Scene:
3263//| """Retained-mode scene with dirty-rectangle rendering. Add sprites and
3364//| tilemaps once (tilemaps first = bottom layer), mutate them each frame,
@@ -59,33 +90,27 @@ static mp_obj_t picogame_scene_make_new(const mp_obj_type_t *type, size_t n_args
5990 mp_arg_val_t args [MP_ARRAY_SIZE (allowed_args )];
6091 mp_arg_parse_all_kw_array (n_args , n_kw , all_args , MP_ARRAY_SIZE (allowed_args ), allowed_args , args );
6192
62- // Accept either the fast picogame.Display (DMA, where available) or a plain
63- // busdisplay (rendered via the portable bus.send fallback -> cross-port).
64- mp_obj_t disp = args [ARG_display ].u_obj ;
65- bool fast = false;
66- #if CIRCUITPY_PICOGAME_FAST_DISPLAY
67- if (mp_obj_is_type (disp , & picogame_display_type )) {
68- fast = true;
69- } else
70- #endif
71- {
72- // Plain busdisplay path (no fast DMA backend, or one passed directly): accept a SUBCLASS
73- // too (e.g. adafruit_st7789.ST7789, a Python subclass of busdisplay.BusDisplay) by casting
74- // to its native base. Store the native object, since the portable renderer below treats
75- // self->display as a busdisplay_busdisplay_obj_t directly.
76- mp_obj_t native = mp_obj_cast_to_native_base (disp , & busdisplay_busdisplay_type );
77- if (!mp_obj_is_type (native , & busdisplay_busdisplay_type )) {
78- mp_raise_TypeError (MP_ERROR_TEXT ("expected a BusDisplay" ));
79- }
80- disp = native ;
93+ // Resolve the render backend (fast Display / Framebuffer / busdisplay) - all the
94+ // flag-gated type checks are concentrated in scene_resolve_target().
95+ bool fast , fb_target ;
96+ mp_obj_t disp = scene_resolve_target (args [ARG_display ].u_obj , & fast , & fb_target );
97+
98+ // The framebuffer path composites directly into the target, so it needs no strip
99+ // buffers; the SPI/DMA paths do. Validate them only when they'll be used. (When
100+ // CIRCUITPY_PICOGAME_FRAMEBUFFER is off, fb_target is always false -> always validated,
101+ // as before.)
102+ if (!fb_target ) {
103+ mp_buffer_info_t tmp ;
104+ mp_get_buffer_raise (args [ARG_buffer_a ].u_obj , & tmp , MP_BUFFER_WRITE );
105+ mp_get_buffer_raise (args [ARG_buffer_b ].u_obj , & tmp , MP_BUFFER_WRITE );
81106 }
82- mp_buffer_info_t tmp ;
83- mp_get_buffer_raise (args [ARG_buffer_a ].u_obj , & tmp , MP_BUFFER_WRITE );
84- mp_get_buffer_raise (args [ARG_buffer_b ].u_obj , & tmp , MP_BUFFER_WRITE );
85107
86108 picogame_scene_obj_t * self = mp_obj_malloc (picogame_scene_obj_t , type );
87109 self -> display = disp ;
88110 self -> fast = fast ;
111+ #if CIRCUITPY_PICOGAME_FRAMEBUFFER
112+ self -> fb_target = fb_target ;
113+ #endif
89114 self -> buf_a = args [ARG_buffer_a ].u_obj ;
90115 self -> buf_b = args [ARG_buffer_b ].u_obj ;
91116 self -> background = args [ARG_background ].u_int ;
@@ -271,36 +296,18 @@ static mp_obj_t picogame_scene_get_display(mp_obj_t self_in) {
271296static MP_DEFINE_CONST_FUN_OBJ_1 (picogame_scene_get_display_obj , picogame_scene_get_display ) ;
272297MP_PROPERTY_GETTER (picogame_scene_display_obj , (mp_obj_t )& picogame_scene_get_display_obj );
273298
274- //| def refresh(self) -> Optional[list]:
275- //| """Diff against the previous frame and repaint only the changed region(s).
276- //| Returns the bounding dirty rect as a REUSED list [x1, y1, x2, y2] (read it
277- //| immediately; it's overwritten next call), or None if nothing changed."""
278- //| ...
279- static mp_obj_t picogame_scene_refresh (mp_obj_t self_in ) {
280- picogame_scene_obj_t * self = MP_OBJ_TO_PTR (self_in );
281-
282- // Resolve the underlying busdisplay from either backend.
283- busdisplay_busdisplay_obj_t * bd ;
284- #if CIRCUITPY_PICOGAME_FAST_DISPLAY
285- picogame_display_obj_t * disp = NULL ;
286- if (self -> fast ) {
287- disp = MP_OBJ_TO_PTR (self -> display );
288- bd = disp -> display ;
289- } else
290- #endif
291- {
292- bd = MP_OBJ_TO_PTR (self -> display );
293- }
294- int w = bd -> core .width ;
295- int h = bd -> core .height ;
296-
297- picogame_rect_t rects [PICOGAME_MAX_DIRTY_RECTS ];
299+ // Compute the frame's dirty rectangles for a WxH target, clipped to the play rect
300+ // [left, w-right) x [top, h-bottom). On the first refresh (or after invalidate) this is
301+ // a single full-screen rect + a snapshot sync; afterwards it diffs against the previous
302+ // frame. Returns the rect count written to `rects` (0 = nothing to repaint).
303+ // Backend-agnostic, so the SPI/fast and framebuffer paths share the exact dirty logic.
304+ static int scene_collect_dirty_rects (picogame_scene_obj_t * self , int w , int h , picogame_rect_t * rects ) {
298305 int nr ;
299306 if (self -> cleared ) {
300307 nr = picogame_scene_compute_dirty_rects (self -> items , self -> kinds , self -> snap ,
301308 self -> count , w , h , self -> ox , self -> oy , rects , PICOGAME_MAX_DIRTY_RECTS );
302309 if (nr == 0 ) {
303- return mp_const_none ;
310+ return 0 ;
304311 }
305312 } else {
306313 rects [0 ].x1 = 0 ;
@@ -333,7 +340,96 @@ static mp_obj_t picogame_scene_refresh(mp_obj_t self_in) {
333340 rects [kept ].y2 = ry2 ;
334341 kept ++ ;
335342 }
336- nr = kept ;
343+ return kept ;
344+ }
345+
346+ // Store the bounding union of `nr` rects into the reusable dirty_rect list (no per-frame
347+ // tuple allocation) and return it. Shared by both refresh backends.
348+ static mp_obj_t scene_store_dirty (picogame_scene_obj_t * self , const picogame_rect_t * rects , int nr , int w , int h ) {
349+ int ux1 = w , uy1 = h , ux2 = 0 , uy2 = 0 ;
350+ for (int i = 0 ; i < nr ; i ++ ) {
351+ if (rects [i ].x1 < ux1 ) {
352+ ux1 = rects [i ].x1 ;
353+ }
354+ if (rects [i ].y1 < uy1 ) {
355+ uy1 = rects [i ].y1 ;
356+ }
357+ if (rects [i ].x2 > ux2 ) {
358+ ux2 = rects [i ].x2 ;
359+ }
360+ if (rects [i ].y2 > uy2 ) {
361+ uy2 = rects [i ].y2 ;
362+ }
363+ }
364+ mp_obj_list_store (self -> dirty_rect , MP_OBJ_NEW_SMALL_INT (0 ), MP_OBJ_NEW_SMALL_INT (ux1 ));
365+ mp_obj_list_store (self -> dirty_rect , MP_OBJ_NEW_SMALL_INT (1 ), MP_OBJ_NEW_SMALL_INT (uy1 ));
366+ mp_obj_list_store (self -> dirty_rect , MP_OBJ_NEW_SMALL_INT (2 ), MP_OBJ_NEW_SMALL_INT (ux2 ));
367+ mp_obj_list_store (self -> dirty_rect , MP_OBJ_NEW_SMALL_INT (3 ), MP_OBJ_NEW_SMALL_INT (uy2 ));
368+ return self -> dirty_rect ;
369+ }
370+
371+ #if CIRCUITPY_PICOGAME_FRAMEBUFFER
372+ // Retained-mode refresh against a RAM framebuffer target (scanout-buffer platforms:
373+ // WASM playground, desktop sim, FruitJam DVI/HSTX). Same dirty-rect logic as the SPI
374+ // path (via the shared helpers), but each rect is composited straight into the target
375+ // with picogame_render_framebuffer - no strip buffers, no bus transaction. A latched
376+ // StripDraw BaseException is re-raised (no bus to close). Self-contained here so
377+ // picogame_scene_refresh() keeps its original SPI/fast shape.
378+ static mp_obj_t scene_refresh_fb (picogame_scene_obj_t * self ) {
379+ picogame_framebuffer_obj_t * fbt = MP_OBJ_TO_PTR (self -> display );
380+ int w = fbt -> width ;
381+ int h = fbt -> height ;
382+
383+ picogame_rect_t rects [PICOGAME_MAX_DIRTY_RECTS ];
384+ int nr = scene_collect_dirty_rects (self , w , h , rects );
385+ if (nr == 0 ) {
386+ return mp_const_none ;
387+ }
388+
389+ for (int i = 0 ; i < nr ; i ++ ) {
390+ mp_obj_t exc = picogame_render_framebuffer (fbt -> fb , fbt -> width , fbt -> height ,
391+ self -> items , self -> kinds , self -> count ,
392+ rects [i ].x1 , rects [i ].y1 , rects [i ].x2 , rects [i ].y2 ,
393+ self -> background , self -> ox , self -> oy );
394+ if (exc != MP_OBJ_NULL ) {
395+ nlr_raise (MP_OBJ_TO_PTR (exc ));
396+ }
397+ }
398+ return scene_store_dirty (self , rects , nr , w , h );
399+ }
400+ #endif // CIRCUITPY_PICOGAME_FRAMEBUFFER
401+
402+ //| def refresh(self) -> Optional[list]:
403+ //| """Diff against the previous frame and repaint only the changed region(s).
404+ //| Returns the bounding dirty rect as a REUSED list [x1, y1, x2, y2] (read it
405+ //| immediately; it's overwritten next call), or None if nothing changed."""
406+ //| ...
407+ static mp_obj_t picogame_scene_refresh (mp_obj_t self_in ) {
408+ picogame_scene_obj_t * self = MP_OBJ_TO_PTR (self_in );
409+
410+ #if CIRCUITPY_PICOGAME_FRAMEBUFFER
411+ if (self -> fb_target ) {
412+ return scene_refresh_fb (self );
413+ }
414+ #endif
415+
416+ // Resolve the underlying busdisplay from either backend.
417+ busdisplay_busdisplay_obj_t * bd ;
418+ #if CIRCUITPY_PICOGAME_FAST_DISPLAY
419+ picogame_display_obj_t * disp = NULL ;
420+ if (self -> fast ) {
421+ disp = MP_OBJ_TO_PTR (self -> display );
422+ bd = disp -> display ;
423+ } else
424+ #endif
425+ {
426+ bd = MP_OBJ_TO_PTR (self -> display );
427+ }
428+ int w = bd -> core .width ;
429+ int h = bd -> core .height ;
430+
431+ picogame_rect_t rects [PICOGAME_MAX_DIRTY_RECTS ];
432+ int nr = scene_collect_dirty_rects (self , w , h , rects );
337433 if (nr == 0 ) {
338434 return mp_const_none ;
339435 }
@@ -347,7 +443,6 @@ static mp_obj_t picogame_scene_refresh(mp_obj_t self_in) {
347443
348444 // Render each dirty rect independently; return their bounding union (kept for
349445 // the existing "dirty WxH" debug prints).
350- int ux1 = w , uy1 = h , ux2 = 0 , uy2 = 0 ;
351446 for (int i = 0 ; i < nr ; i ++ ) {
352447 #if CIRCUITPY_PICOGAME_FAST_DISPLAY
353448 if (self -> fast ) {
@@ -364,26 +459,9 @@ static mp_obj_t picogame_scene_refresh(mp_obj_t self_in) {
364459 rects [i ].x1 , rects [i ].y1 , rects [i ].x2 , rects [i ].y2 ,
365460 self -> background , self -> ox , self -> oy );
366461 }
367- if (rects [i ].x1 < ux1 ) {
368- ux1 = rects [i ].x1 ;
369- }
370- if (rects [i ].y1 < uy1 ) {
371- uy1 = rects [i ].y1 ;
372- }
373- if (rects [i ].x2 > ux2 ) {
374- ux2 = rects [i ].x2 ;
375- }
376- if (rects [i ].y2 > uy2 ) {
377- uy2 = rects [i ].y2 ;
378- }
379462 }
380463
381- // Update the reusable list in place - no per-frame tuple allocation.
382- mp_obj_list_store (self -> dirty_rect , MP_OBJ_NEW_SMALL_INT (0 ), MP_OBJ_NEW_SMALL_INT (ux1 ));
383- mp_obj_list_store (self -> dirty_rect , MP_OBJ_NEW_SMALL_INT (1 ), MP_OBJ_NEW_SMALL_INT (uy1 ));
384- mp_obj_list_store (self -> dirty_rect , MP_OBJ_NEW_SMALL_INT (2 ), MP_OBJ_NEW_SMALL_INT (ux2 ));
385- mp_obj_list_store (self -> dirty_rect , MP_OBJ_NEW_SMALL_INT (3 ), MP_OBJ_NEW_SMALL_INT (uy2 ));
386- return self -> dirty_rect ;
464+ return scene_store_dirty (self , rects , nr , w , h );
387465}
388466static MP_DEFINE_CONST_FUN_OBJ_1 (picogame_scene_refresh_obj , picogame_scene_refresh ) ;
389467
0 commit comments