Skip to content

Commit d43bca9

Browse files
committed
picogame: optional Framebuffer.sync hook for tear-free full repaints
Framebuffer gains a settable `sync` attribute: an optional 0-arg callable that Scene.refresh() invokes on the framebuffer path BEFORE compositing a large (>= half-screen) region - e.g. a camera move that repaints most of the frame. Small sprite-only dirty rects skip it, so per-frame latency is unaffected. This decouples the engine from any specific backend (the plan's guidance): the platform supplies the callable - on Fruit Jam picogame_game.setup() wires it to picodvi Framebuffer.wait_for_vblank(), so a full repaint starts at the DVI frame boundary and races ahead of scanout instead of tearing mid-screen. None = off (SPI panels, sim, older firmware). Addresses the reported camera-move tearing; pairs with the picodvi wait_for_vblank() primitive.
1 parent a1d06e4 commit d43bca9

3 files changed

Lines changed: 36 additions & 0 deletions

File tree

‎shared-bindings/picogame/Scene.c‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,20 @@ static mp_obj_t scene_refresh_fb(picogame_scene_obj_t *self) {
422422
// Snapshots are already advanced; stay in the needs-full-repaint state until the
423423
// render completes, so a BaseException mid-render (Ctrl-C in a StripDraw) leaves a
424424
// scene whose NEXT refresh repaints everything instead of keeping a torn frame.
425+
// Before compositing a LARGE region (e.g. a camera move that repaints most of the frame),
426+
// invoke the framebuffer's optional sync hook (e.g. picodvi wait_for_vblank) so the repaint
427+
// starts at a frame boundary and races ahead of scanout instead of tearing mid-screen. Small
428+
// sprite-only dirty rects skip it - the wait would only add latency with nothing to tear.
429+
if (fbt->sync != mp_const_none) {
430+
int area = 0;
431+
for (int i = 0; i < nr; i++) {
432+
area += (rects[i].x2 - rects[i].x1) * (rects[i].y2 - rects[i].y1);
433+
}
434+
if (area * 2 >= w * h) { // >= half the screen -> treat as a full repaint
435+
mp_call_function_0(fbt->sync);
436+
}
437+
}
438+
425439
self->cleared = false;
426440
for (int i = 0; i < nr; i++) {
427441
mp_obj_t exc = picogame_render_framebuffer(fbt->fb, fbt->width, fbt->height, fbt->native_rgb565,

‎shared-bindings/picogame/__init__.c‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,7 @@ static mp_obj_t picogame_framebuffer_make_new(const mp_obj_type_t *type, size_t
14001400
self->width = width;
14011401
self->height = height;
14021402
self->native_rgb565 = args[ARG_native_rgb565].u_bool;
1403+
self->sync = mp_const_none;
14031404
return MP_OBJ_FROM_PTR(self);
14041405
}
14051406

@@ -1415,9 +1416,27 @@ static mp_obj_t picogame_framebuffer_get_height(mp_obj_t self_in) {
14151416
static MP_DEFINE_CONST_FUN_OBJ_1(picogame_framebuffer_get_height_obj, picogame_framebuffer_get_height);
14161417
MP_PROPERTY_GETTER(picogame_framebuffer_height_obj, (mp_obj_t)&picogame_framebuffer_get_height_obj);
14171418

1419+
//| sync: Optional[Callable[[], None]]
1420+
//| """An optional 0-arg callable invoked right before a large/full-screen region is
1421+
//| composited (e.g. a picodvi ``Framebuffer.wait_for_vblank``). Lets a full repaint start at a
1422+
//| frame boundary so it does not tear, with no cost on small (sprite-only) frames. ``None`` = off."""
1423+
static mp_obj_t picogame_framebuffer_get_sync(mp_obj_t self_in) {
1424+
return ((picogame_framebuffer_obj_t *)MP_OBJ_TO_PTR(self_in))->sync;
1425+
}
1426+
static MP_DEFINE_CONST_FUN_OBJ_1(picogame_framebuffer_get_sync_obj, picogame_framebuffer_get_sync);
1427+
static mp_obj_t picogame_framebuffer_set_sync(mp_obj_t self_in, mp_obj_t value) {
1428+
((picogame_framebuffer_obj_t *)MP_OBJ_TO_PTR(self_in))->sync = value;
1429+
return mp_const_none;
1430+
}
1431+
static MP_DEFINE_CONST_FUN_OBJ_2(picogame_framebuffer_set_sync_obj, picogame_framebuffer_set_sync);
1432+
MP_PROPERTY_GETSET(picogame_framebuffer_sync_obj,
1433+
(mp_obj_t)&picogame_framebuffer_get_sync_obj,
1434+
(mp_obj_t)&picogame_framebuffer_set_sync_obj);
1435+
14181436
static const mp_rom_map_elem_t picogame_framebuffer_locals_dict_table[] = {
14191437
{ MP_ROM_QSTR(MP_QSTR_width), MP_ROM_PTR(&picogame_framebuffer_width_obj) },
14201438
{ MP_ROM_QSTR(MP_QSTR_height), MP_ROM_PTR(&picogame_framebuffer_height_obj) },
1439+
{ MP_ROM_QSTR(MP_QSTR_sync), MP_ROM_PTR(&picogame_framebuffer_sync_obj) },
14211440
};
14221441
static MP_DEFINE_CONST_DICT(picogame_framebuffer_locals_dict, picogame_framebuffer_locals_dict_table);
14231442

‎shared-module/picogame/__init__.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ typedef struct {
178178
int height;
179179
bool native_rgb565; // false = wire-order (default); true = each finished region is
180180
// byte-swapped to NATIVE RGB565 (picodvi / canvas scanout targets)
181+
mp_obj_t sync; // optional 0-arg callable, invoked BEFORE compositing a large/full-screen
182+
// region (e.g. picodvi Framebuffer.wait_for_vblank) so a full repaint
183+
// starts at a frame boundary and does not tear; mp_const_none = no sync
181184
} picogame_framebuffer_obj_t;
182185

183186
// Full-frame RAM-framebuffer backend: same layered compositor as the SPI strip path

0 commit comments

Comments
 (0)