diff --git a/applications/PoP/docs/double_buffer_plan.md b/applications/PoP/docs/double_buffer_plan.md new file mode 100644 index 0000000..33894c8 --- /dev/null +++ b/applications/PoP/docs/double_buffer_plan.md @@ -0,0 +1,69 @@ +# Double buffer (два экрана + флип) — план + +Цель: убрать мерцание/тиринг при перерисовке слоёв (Kid ↔ fore/пол-оверлей) +и гарантировать, что на экране ВСЕГДА готовый кадр с правильным порядком +слоёв. Нужно для отладки fore-слоя (видно момент композиции, а не +промежуточные состояния heal-рендера). **Тумблер обязателен** — +однобуферный режим удобнее для отладки багов рисования. + +## Что уже готово (libbgi — трогать НЕ нужно) + +- Две графических страницы 0/1: `gfx_set_draw_page(p)` (двигает + `_gfx_addr_base` 0xC000/0xC140 — рисуют все примитивы), + `gfx_set_visible_page(p)` (ESTEX $54 SELPAGE — display-учёт DSS). +- `gfx_wait_vsync()` — луч (bit5 порта 0xFE), момент без разрывов. +- Паттерн из gfx.h: `set_draw_page(hidden); draw(); wait_vsync(); + set_visible_page(hidden);` +- Замечание gfx.h: у каждой страницы СВОЯ плоскость палитры + (page0→pal0, page1→pal1) — для seamless грузить одну палитру в ОБЕ. + +## Текущая модель рендера roomtest (однобуфер) + +`roomtest.c`: `draw_page=0`, `visible_page=0` фиксированы. Фон комнаты +рисуется ОДИН раз в видео-ОЗУ + теневую копию (GFX_BANK_TRANSPARENT). +Цикл: 3× `gfx_wait_vsync` (пейсинг) → `pop_ctrl_tick` → `kid_heal()` +(восстановить прямоугольник Кида из тени) → `kid_tick`/`pop_phys_tick` +→ `kid_draw` → `pop_fore_over_kid`. Мерцание = heal+draw+fore длиннее +бланка, луч ловит промежуток. + +## Работа на стороне PoP + +1. **Инициализация обеих страниц**: `pop_room_draw` в page 0 И page 1 + (теневая копия одна — общая, из неё heal'ит любая страница). +2. **Палитра в обе плоскости**: сейчас `gfx_pal_fload(0,...)` + sync. + Продублировать в plane 1 (проверить сигнатуру gfx_pal_fload/sync — + plane-параметр). +3. **Per-page heal-история** (ядро): вынести `kid_lx/ly/lw/lh` в + массивы `[2]`, индекс = страница, в которую рисуем. `kid_heal(page)` + восстанавливает СВОЙ прошлый прямоугольник (кадр -2, т.к. рисуем + через страницу). То же для fore/пол-оверлея, если они рисуют вне + футпринта Кида. +4. **Ping-pong в цикле**: + ``` + uint8_t back = dbuf ? (front ^ 1) : 0; + gfx_set_draw_page(back); + kid_heal(back); kid_tick; phys; kid_draw; fore; + gfx_wait_vsync(); + if (dbuf) { gfx_set_visible_page(back); front = back; } + ``` +5. **Тумблер** `dbuf`: off → draw==visible==0, без флипа, heal[0] — + бит-в-бит текущее поведение. Управление — клавишей (напр. F2) или + compile-флагом. +6. **Пейсинг**: сейчас 3× vsync/лог.кадр. При флипе — один vsync перед + свопом; недостающий пейсинг добрать `gfx_set_fps_div(3)` или ручным + счётом кадров, чтобы скорость игры не изменилась. + +## Порядок + +- D1: обе страницы + палитра в обе плоскости; ping-pong без per-page + heal (проверить, что флип работает, фон корректен на обеих). +- D2: per-page heal-история (kid_l*[2]) — убрать «хвост» Кида. +- D3: тумблер dbuf + сверка однобуферного пути с текущим (регресс-нет). +- D4: пейсинг (fps_div) — вернуть исходную скорость. + +## Связанные + +Рендер-модель — [[pop_fore_layer]], [[pop_background_strategy]]; +heal — kid_heal/gfx_heal. Будущие динамические слои (loose-floors +[[двойной буфер требует их per-page перерисовки]]) должны рисоваться в +обе страницы по той же дисциплине. diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 39deb84..3c3aff7 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -163,9 +163,14 @@ void kid_tick(void) #define KID_XOFF 0 #define KID_YOFF 0 -static int kid_lx, kid_ly; -static uint16_t kid_lw, kid_lh; -static uint8_t kid_lvalid; +/* Прямоугольник последней отрисовки Kid — ПО СТРАНИЦЕ (0/1). У каждой + * графической страницы своя ОЗУ-копия/видео-ОЗУ, поэтому heal должен + * стирать пиксели Kid именно той страницы, в которую сейчас рисуем + * (при дабл-буфере страница чередуется — прошлый Kid на этой странице + * был 2 логических кадра назад). В однобуфере используется только [0]. */ +static int kid_lx[2], kid_ly[2]; +static uint16_t kid_lw[2], kid_lh[2]; +static uint8_t kid_lvalid[2]; /* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg): * obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */ @@ -179,9 +184,10 @@ uint16_t kid_fp_height(void) { return kid_fp_h; } /* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */ void kid_heal(void) { - if (kid_lvalid) - gfx_heal(kid_lx, kid_ly, kid_lw, kid_lh); - kid_lvalid = 0; + uint8_t p = gfx_get_draw_page(); + if (kid_lvalid[p]) + gfx_heal(kid_lx[p], kid_ly[p], kid_lw[p], kid_lh[p]); + kid_lvalid[p] = 0; } void kid_draw(void) @@ -221,11 +227,12 @@ void kid_draw(void) if (flip) bx -= (int)w; /* seg008: xpos -= image->w */ top = obj_y - (int)h + 1 + KID_YOFF; + uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */ gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ gfx_blit_cols(bx, top, img, flip); gfx_set_bank(GFX_BANK_NORMAL); - kid_lx = bx; kid_ly = top; - kid_lw = w; kid_lh = h; kid_lvalid = 1; + kid_lx[dp] = bx; kid_ly[dp] = top; + kid_lw[dp] = w; kid_lh[dp] = h; kid_lvalid[dp] = 1; kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */ } gfx_w0_unmap(); diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 960a467..280ec48 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -20,6 +20,11 @@ int main(void) { + /* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1 + * по умолчанию (без мерцания); SPACE переключает в однобуфер для + * отладки багов рисования (edge по sp_prev, чтоб не дёргать). */ + uint8_t pg, front = 0, back, dbuf = 1, sp_prev = 0; + if (pop_bg_load() != 0) { /* атласы — ДО initgraph */ puts("pop_bg_load failed"); return 1; @@ -30,19 +35,22 @@ int main(void) } initgraph(); - gfx_set_draw_page(0); gfx_set_visible_page(0); if (gfx_pal_fload(0, "kid.pal") < 0) /* единая игровая палитра */ gfx_pal_fload(0, "a:\\kid.pal"); - gfx_pal_sync(); + gfx_pal_sync(); /* палитра 0 -> плоскость 1 */ - /* Фон комнаты рисуется ОДИН РАЗ — в видео-ОЗУ И теневую копию - * (GFX_BANK_TRANSPARENT внутри pop_room_draw). Дальше каждый тик - * восстанавливаем ТОЛЬКО прямоугольник Кида из теневой копии (heal), - * а не весь экран. */ - setfillstyle(SOLID_FILL, BLACK); - bar(0, 0, 319, 255); - pop_room_draw(1, room1_fg, room1_bg, leftcol_fg, leftcol_bg); + /* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя + * ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ + * страницы, в которую рисуем. Дальше каждый тик восстанавливаем + * ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */ + for (pg = 0; pg < 2; pg++) { + gfx_set_draw_page(pg); + setfillstyle(SOLID_FILL, BLACK); + bar(0, 0, 319, 255); + pop_room_draw(1, room1_fg, room1_bg, leftcol_fg, leftcol_bg); + } + gfx_set_draw_page(0); /* K2b: Kid стоит на уступе (row1), управляется с клавиатуры. * ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход. */ @@ -59,11 +67,24 @@ int main(void) } while (!kbd_raw_down(KBD_ESC)) { - gfx_wait_vsync(); - gfx_wait_vsync(); - gfx_wait_vsync(); + /* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один + * переключ). Выключение возвращает показ на страницу 0. */ + uint8_t sp = kbd_raw_down(KBD_SPACE); + if (sp && !sp_prev) { + dbuf ^= 1; + if (!dbuf) { + gfx_set_visible_page(0); + front = 0; + } + } + sp_prev = sp; + + /* Рисуем в СКРЫТУЮ страницу (при dbuf); в однобуфере — в 0. */ + back = dbuf ? (uint8_t)(front ^ 1) : 0; + gfx_set_draw_page(back); + pop_ctrl_tick(); /* ввод -> control(): смена seq */ - kid_heal(); /* стереть прошлый кадр (фон из тени) */ + kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */ kid_tick(); /* play_seq: следующий кадр */ pop_phys_tick(); /* K3: падение/приземление/стена */ if (pop_fell_out) { /* упал за низ комнаты — сброс на старт */ @@ -73,6 +94,17 @@ int main(void) kid_draw(); /* нарисовать новый кадр */ pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), kid_fp_width(), kid_fp_height(), Kid.direction, Kid.frame); + + /* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже + * сделан (скрытая страница), wait_vsync ловит начало кадра, + * set_visible_page переключает во время бланка (gfx.h паттерн). */ + gfx_wait_vsync(); + gfx_wait_vsync(); + gfx_wait_vsync(); + if (dbuf) { + gfx_set_visible_page(back); + front = back; + } } pop_ctrl_close();