roomtest: двойная буферизация (два экрана + флип), тумблер SPACE
Убирает мерцание/тиринг при перерисовке слоёв (Kid/fore/пол-оверлей): рендер всего кадра в скрытую страницу, tear-free флип на vblank. Инфра libbgi (gfx_set_draw_page/visible_page/wait_vsync) уже была. - фон комнаты рисуется в ОБЕ графические страницы (у каждой своя ОЗУ-копия — источник heal); палитра 0->1 уже синкалась gfx_pal_sync; - kid_heal/kid_draw: прямоугольник Kid запоминается ПО СТРАНИЦЕ (kid_l*[2]) — при чередовании страниц heal стирает пиксели своей страницы (прошлый Kid там был 2 логических кадра назад); - цикл: draw в back, 3x wait_vsync (пейсинг), gfx_set_visible_page(back); - SPACE (edge) — тумблер: off = однобуфер (draw==visible==0) для отладки. Мерцание при подъёме подтверждено устранённым в MAME (пользователь). План: applications/PoP/docs/double_buffer_plan.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 перерисовки]]) должны рисоваться в
|
||||||
|
обе страницы по той же дисциплине.
|
||||||
@@ -163,9 +163,14 @@ void kid_tick(void)
|
|||||||
#define KID_XOFF 0
|
#define KID_XOFF 0
|
||||||
#define KID_YOFF 0
|
#define KID_YOFF 0
|
||||||
|
|
||||||
static int kid_lx, kid_ly;
|
/* Прямоугольник последней отрисовки Kid — ПО СТРАНИЦЕ (0/1). У каждой
|
||||||
static uint16_t kid_lw, kid_lh;
|
* графической страницы своя ОЗУ-копия/видео-ОЗУ, поэтому heal должен
|
||||||
static uint8_t kid_lvalid;
|
* стирать пиксели 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):
|
/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
|
||||||
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
|
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
|
||||||
@@ -179,9 +184,10 @@ uint16_t kid_fp_height(void) { return kid_fp_h; }
|
|||||||
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */
|
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */
|
||||||
void kid_heal(void)
|
void kid_heal(void)
|
||||||
{
|
{
|
||||||
if (kid_lvalid)
|
uint8_t p = gfx_get_draw_page();
|
||||||
gfx_heal(kid_lx, kid_ly, kid_lw, kid_lh);
|
if (kid_lvalid[p])
|
||||||
kid_lvalid = 0;
|
gfx_heal(kid_lx[p], kid_ly[p], kid_lw[p], kid_lh[p]);
|
||||||
|
kid_lvalid[p] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void kid_draw(void)
|
void kid_draw(void)
|
||||||
@@ -221,11 +227,12 @@ void kid_draw(void)
|
|||||||
if (flip)
|
if (flip)
|
||||||
bx -= (int)w; /* seg008: xpos -= image->w */
|
bx -= (int)w; /* seg008: xpos -= image->w */
|
||||||
top = obj_y - (int)h + 1 + KID_YOFF;
|
top = obj_y - (int)h + 1 + KID_YOFF;
|
||||||
|
uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */
|
||||||
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
|
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
|
||||||
gfx_blit_cols(bx, top, img, flip);
|
gfx_blit_cols(bx, top, img, flip);
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
kid_lx = bx; kid_ly = top;
|
kid_lx[dp] = bx; kid_ly[dp] = top;
|
||||||
kid_lw = w; kid_lh = h; kid_lvalid = 1;
|
kid_lw[dp] = w; kid_lh[dp] = h; kid_lvalid[dp] = 1;
|
||||||
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
|
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
|
||||||
}
|
}
|
||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
|
|||||||
@@ -20,6 +20,11 @@
|
|||||||
|
|
||||||
int main(void)
|
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 */
|
if (pop_bg_load() != 0) { /* атласы — ДО initgraph */
|
||||||
puts("pop_bg_load failed");
|
puts("pop_bg_load failed");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -30,19 +35,22 @@ int main(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
initgraph();
|
initgraph();
|
||||||
gfx_set_draw_page(0);
|
|
||||||
gfx_set_visible_page(0);
|
gfx_set_visible_page(0);
|
||||||
if (gfx_pal_fload(0, "kid.pal") < 0) /* единая игровая палитра */
|
if (gfx_pal_fload(0, "kid.pal") < 0) /* единая игровая палитра */
|
||||||
gfx_pal_fload(0, "a:\\kid.pal");
|
gfx_pal_fload(0, "a:\\kid.pal");
|
||||||
gfx_pal_sync();
|
gfx_pal_sync(); /* палитра 0 -> плоскость 1 */
|
||||||
|
|
||||||
/* Фон комнаты рисуется ОДИН РАЗ — в видео-ОЗУ И теневую копию
|
/* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя
|
||||||
* (GFX_BANK_TRANSPARENT внутри pop_room_draw). Дальше каждый тик
|
* ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ
|
||||||
* восстанавливаем ТОЛЬКО прямоугольник Кида из теневой копии (heal),
|
* страницы, в которую рисуем. Дальше каждый тик восстанавливаем
|
||||||
* а не весь экран. */
|
* ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */
|
||||||
setfillstyle(SOLID_FILL, BLACK);
|
for (pg = 0; pg < 2; pg++) {
|
||||||
bar(0, 0, 319, 255);
|
gfx_set_draw_page(pg);
|
||||||
pop_room_draw(1, room1_fg, room1_bg, leftcol_fg, leftcol_bg);
|
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), управляется с клавиатуры.
|
/* K2b: Kid стоит на уступе (row1), управляется с клавиатуры.
|
||||||
* ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход. */
|
* ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход. */
|
||||||
@@ -59,11 +67,24 @@ int main(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (!kbd_raw_down(KBD_ESC)) {
|
while (!kbd_raw_down(KBD_ESC)) {
|
||||||
gfx_wait_vsync();
|
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
||||||
gfx_wait_vsync();
|
* переключ). Выключение возвращает показ на страницу 0. */
|
||||||
gfx_wait_vsync();
|
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 */
|
pop_ctrl_tick(); /* ввод -> control(): смена seq */
|
||||||
kid_heal(); /* стереть прошлый кадр (фон из тени) */
|
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
|
||||||
kid_tick(); /* play_seq: следующий кадр */
|
kid_tick(); /* play_seq: следующий кадр */
|
||||||
pop_phys_tick(); /* K3: падение/приземление/стена */
|
pop_phys_tick(); /* K3: падение/приземление/стена */
|
||||||
if (pop_fell_out) { /* упал за низ комнаты — сброс на старт */
|
if (pop_fell_out) { /* упал за низ комнаты — сброс на старт */
|
||||||
@@ -73,6 +94,17 @@ int main(void)
|
|||||||
kid_draw(); /* нарисовать новый кадр */
|
kid_draw(); /* нарисовать новый кадр */
|
||||||
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
|
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
|
||||||
kid_fp_width(), kid_fp_height(), Kid.direction, Kid.frame);
|
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();
|
pop_ctrl_close();
|
||||||
|
|||||||
Reference in New Issue
Block a user