/* * gfx_wait_vsync — ждать переход бита 5 порта 0xFE из 1 в 0: выход из * нижнего blank/border (Y > 256 — см. MAME sprinter.cpp kbd_fe_r: * `vpos() >= BORDER_TOP+SCREEN_YSIZE`) — начало нового кадра, идеальный * момент для свопа страниц без разрывов. Это чтение РЕАЛЬНОГО * состояния луча (не событие/прерывание), поэтому не подвержено * путанице с другими источниками вектора 0xFF (клавиатура, CBL, CTC) — * подробнее см. memory/gfx_wait_vsync_redesign. * * Бит доступен ТОЛЬКО пока включён cbl_mode() (bit7 порта 0x004E) — * см. libc/cbl/_cbl_port.c: если приложение уже играет через * cbl_open(), берём бит бесплатно (порт не трогаем); иначе * лениво занимаем bit7 "немым" кодом частоты через _cbl_port_ref() * при первом вызове, освобождаем в atexit. * * Fallback: если ни разу не увидели ожидаемое состояние за разумное * число попыток (иное железо, бит ведёт себя не как в MAME) — * откатываемся на одно кадровое прерывание (halt), чтобы не * зависнуть намертво. * * FPS-ДЕЛИТЕЛЬ (docs/sprite-api-design.md §9е): при _gfx_fps_div>=2 * (выставлен gfx_set_fps_div) идём СЧЁТЧИКОВЫМ путём — ждём, пока * фоновый ISR насчитает n кадровых фронтов от прошлого выхода. В * отличие от поллинга это ловит фронты, прошедшие ВО ВРЕМЯ рендера * (elapsed>=n → выравнивание на ближайший фронт, без накопления фазы). */ #include "../_bgi.h" #include static uint8_t vsync_ready; static void vsync_cleanup(void) { _cbl_port_unref(); } /* EI + HALT: разбудит ЛЮБОЕ прерывание (кадр/клавиатура/CBL), вызывающий * цикл перепроверит счётчик. EI обязателен (как в fallback ниже). */ static void gfx_frame_halt(void) __naked { __asm ei halt ret __endasm; } /* Лучевой поллинг (n<=1) — выделен в отдельную функцию: её тело * заканчивается непрозрачным __asm-блоком, поэтому в НЕЙ не должно быть * переходов «через asm» (SDCC не эмитит эпилог-метку за таким блоком). * Ранний return делителя живёт в gfx_wait_vsync (чистый C). */ static void gfx_wait_vsync_beam(void) { if (!vsync_ready) { vsync_ready = 1; _cbl_port_ref(); atexit(vsync_cleanup); } __asm ;; --- фаза 1: дождаться bit5==1 (гарантированно в blank) --- ld bc, #0 ; ~65536 попыток на фазу — таймаут _gwv_wait1: in a, (#0xFE) bit 5, a jr nz, _gwv_have1 dec bc ld a, b or c jr nz, _gwv_wait1 jr _gwv_fallback ; bit5 ни разу не встал за таймаут _gwv_have1: ;; --- фаза 2: дождаться bit5==0 (сам переход — начало кадра) --- ld bc, #0 _gwv_wait0: in a, (#0xFE) bit 5, a jr z, _gwv_done dec bc ld a, b or c jr nz, _gwv_wait0 ;; таймаут и здесь — переходим к тому же фолбэку _gwv_fallback: ei halt ; аварийный путь: одно кадровое прерывание _gwv_done: __endasm; } void gfx_wait_vsync(void) { /* --- счётчиковый путь делителя (n>=2) --- */ if (_gfx_fps_div >= 2) { uint8_t n = _gfx_fps_div; if ((uint8_t)(_gfx_frame_tick - _gfx_fps_last) >= n) { /* Слот истёк (рендер занял >= n интервалов) — ближайший * фронт, чтобы не копить фазовую ошибку. */ uint8_t t = _gfx_frame_tick; while (_gfx_frame_tick == t) gfx_frame_halt(); } else { while ((uint8_t)(_gfx_frame_tick - _gfx_fps_last) < n) gfx_frame_halt(); } _gfx_fps_last = _gfx_frame_tick; return; } /* --- n<=1: обычный лучевой поллинг (поведение бит-в-бит прежнее) --- */ gfx_wait_vsync_beam(); }