Skip to content

Feature/hd engine#25

Open
mdrobniu wants to merge 3 commits into
usineur:masterfrom
mdrobniu:feature/hd-engine
Open

Feature/hd engine#25
mdrobniu wants to merge 3 commits into
usineur:masterfrom
mdrobniu:feature/hd-engine

Conversation

@mdrobniu
Copy link
Copy Markdown

@mdrobniu mdrobniu commented May 9, 2026

Summary

This PR adds a configurable presentation pipeline on top of the existing
hode engine without altering the original game logic. The 12.5 Hz tick,
data-file format and per-level scripted callbacks are unchanged.

It also fixes a few long-standing UX rough edges (broken keyboard rebinding
flow, font-pointer dereference in the menu, sprite cache key not surviving
across runs) and gets the engine to build cleanly on macOS.

The work is split into two commits on feature/hd-engine so the docs
churn doesn't drown the engine diff.

What's added

HD rendering pipeline (HdCompositor, SpriteUpscaler, edge_smooth)

  • Chained xBRZ scalers (2×, 3×, 4× via 2×·2×, 5× nearest, arbitrary Nx
    via decompose into two passes, 6×..16× presets).
  • MLAA edge smoothing applied after the final scale.
  • Per-screen background upscaled once on screen change; sprites cached in
    RAM (LRU, 512 MB cap) and on disk under cache/<scale>x/.
  • PAF cutscenes upscaled and cached frame-by-frame under
    cache/paf/<scale>x/v<NN>/f<NNNN>.raw.
  • 16:9 widescreen with palette-derived gradient borders (no fake
    gameplay), sampled per-screen so the borders track lighting/mood.

Decoupled smooth animation

  • --smooth runs render at ~60 Hz with linear sprite-position
    interpolation between game ticks. The 12.5 Hz tick is untouched, so
    collision/AI/sound/scripted callbacks behave exactly as before.

Sprite + cutscene prerender

  • --prerender walks every (sprite-type, frame, flip) tuple — including
    every screen's backgroundLvlObjectDataTable[0..7] (per-screen
    background animations like trees / water) — and fast-forward decodes
    the in-gameplay PAF clips (Canyon falling with cannon, Canyon falling,
    Island falling) plus the level intro cutscene.
  • Both phases drive one shared console progress bar:
    prerender level 0 (rock) [████████████████████████████....] 4720/5320 (88%) 5.2s eta 0.7s
    
  • The PAF "watched cutscenes" save mask is snapshotted/restored across
    prerender so your save isn't polluted with movies you didn't watch.
  • After the bar completes, the engine pumps SDL once and re-baselines
    inp.prevMask = inp.mask so any held keys don't fire phantom
    press/release events on the first gameplay tick.

Automation API (automation_api.{h,cpp})

  • Non-blocking AF_UNIX JSON socket. Newline-delimited.
  • Commands: get_state, input (direction/action/raw), step (frame
    stepping), screenshot (256×192 RGB), set_level.
  • --automation also implies disable_menu, loading_screen=false,
    disable_paf=true for fast scripted boot.
  • Four bundled Python drivers under test_*.py.

macOS portability

  • intern.h adds an __APPLE__ branch using <libkern/OSByteOrder.h>.
    Upstream fails to compile on macOS because clang's libc has no
    glibc-style <endian.h>.

What's fixed

Issue Fix
Sprite disk cache never produced cross-run hits SpriteUpscaler cache key changed from (uintptr_t)sprData (heap pointer) to a content hash: FNV-1a over decoded SPR bytes ⊕ dims ⊕ flip flags ⊕ palette FNV-1a.
Same sprite shown on a different palette screen could display with stale colours after my first cache fix Palette FNV-1a is part of the cache key, so different palettes produce different entries.
--hd-wide borders were "still black" The SDL window was 4:3 unless legacy --widescreen was on, so the wide framebuffer was squished into a 4:3 logical surface and the gradient borders disappeared. --hd-wide now also sizes the SDL window 16:9.
HD borders were ~4× too dim HdCompositor::beginFrame() used to pack the engine's 6-bit palette directly into 8-bit RGB without expanding. Now expands 6→8 bits before sampling for computeBorderColors().
Keyboard menu crashed when binding a key Video::_font was only assigned in Game::mainLoop, but the menu (which runs before mainLoop) draws keycode glyphs via drawStringCharacter. Garbage pointer + uncommon key offset = SIGSEGV. Now: zero-init in Video ctor, and wire from _res->_fontBuffer right after loadSetupDat().
After binding one action, no other action could be selected applyKeyboardControls used to wipe the default mappings and only re-add arrows + ESC + the user's custom keys. So if the user bound Z to Run, LCtrl/F/LAlt/G/LShift/H all went dead — meaning the menu's Select handler (which fires on keyReleased(SYS_INP_RUN/JUMP/SHOOT)) couldn't trigger Jump/Shoot binding. Now the defaults stay live alongside user keys.
Phantom re-entry into bind mode after binding After waitForKeyPress returned, inp.prevMask still held the SELECT key the user pressed to enter bind mode; on the next processEvents that registered as a fresh keyReleased(SHOOT) and re-fired SELECT. Now waitForKeyPress snapshots current state into prevMask before returning, so no edges fire.
Esc during bind closed the entire menu and quit the game Same root cause as above; same fix.
You couldn't bind Space / Enter / Tab / Backspace / Cmd-Win scancodeToDisplayCode now accepts these. The bitmap font has no glyphs, so drawKeyboardKeyCode falls back to short text labels (SP, EN, TB, BS, WN).
One key could be assigned to multiple actions When binding a key, any prior assignment of that scancode is cleared first (one key = one action).
OK / Cancel / Test buttons in the keyboard menu were inert _keyboardControlsNum == 3 is now sub-state-routed via a new _kbdButton (0=OK, 1=Cancel, 2=Test); ←/→ cycles, OK keeps changes, Cancel reverts to the snapshot taken on screen entry, Test enters live key-press visualisation (state 8) which now polls g_system->inp.mask instead of being hard-coded to 0.

New CLI flags

Flag Effect
--hd HD compositor at default 6× (1536×1152)
--hd-scale=N HD at custom scale 2..16
--fullhd shortcut for --hd-scale=8
--4k shortcut for --hd-scale=15
--hd-wide 16:9 framebuffer with palette-gradient borders; sizes window 16:9
--hd-cache=PATH persistent disk cache for sprites and PAF frames
--prerender populate sprite + relevant PAF caches per level on first entry
--smooth 60 Hz interpolated render with 12.5 Hz logic
--automation=PATH open Unix-socket JSON server

All also available as [display]/[engine] keys in hode.ini
(hd_mode, hd_scale, hd_widescreen, hd_cache, smooth_anim,
automation_socket).

New / modified files

New:

hd_compositor.{h,cpp}      HD framebuffer, presets, 16:9 borders, prerender
sprite_upscaler.{h,cpp}    xBRZ chains + RAM/disk cache, content-hash key
edge_smooth.{h,cpp}        MLAA edge smoothing
automation_api.{h,cpp}     Unix-socket JSON server
test_walkthrough.py        level 1 walkthrough
test_all_levels.py         per-level boot smoke test
test_combat_bot.py         reactive combat AI
test_hd_replay.py          HD vs SD comparison harness
README.md                  comprehensive front-page docs (replaces README.txt)

Modified:

Makefile         add new sources to SRCS
intern.h         macOS <libkern/OSByteOrder.h> shim
main.cpp         new CLI flags + INI keys; wire _video->_font after loadSetupDat()
game.{h,cpp}     HD compositor begin/end-frame hooks; smooth-anim render loop;
                 prerender driver with shared progress bar
paf.{h,cpp}      HD frame callback path; _prerenderMode skips audio/display/sleep;
                 peekFramesCount(num)
menu.{h,cpp}     keyboard binding overhaul (OK/Cancel/Test, one-key-one-action,
                 Space/Enter/Tab/Backspace/Win bindable, fallback labels)
system.h         expose applyKeyboardControls / waitForKeyPress on the abstract iface
system_sdl2.cpp  defaults stay live alongside user keys; waitForKeyPress clears
                 edge state; copyRectRGBA for HD presents
video.cpp        zero-init _font in ctor

Backwards compatibility

  • Save file (setup.cfg) format unchanged — your saves keep working.
  • Game data (HOD.PAF, *_HOD.*, SETUP.DAT) untouched.
  • Disk cache format changed — old cache/ directories from earlier
    builds (or any pre-existing pointer-keyed cache) won't be hit.
    Delete cache/ before the first run with this PR. The engine will
    rebuild it.
  • PSP / Wii backends are still in the source tree but, like upstream,
    not wired into this Makefile (only system_sdl2.cpp is built).

How to test

# Vanilla, behaves like upstream
./hode

# HD with full prerender + persistent cache
./hode --hd --hd-cache=./cache --prerender

# Widescreen 4K with smooth render
./hode --4k --hd-wide --smooth --hd-cache=./cache --prerender

# Headless scripted (Linux)
SDL_AUDIODRIVER=dummy xvfb-run -a ./hode --automation=/tmp/hode.sock --hd
python test_walkthrough.py

# macOS build sanity check
brew install sdl2
make clean && make -j"$(sysctl -n hw.ncpu)"

The new code is gated behind CLI/INI flags — running without any of the
new flags is bit-identical (modulo the small portability/menu fixes) to
upstream behaviour.

hode-hd added 3 commits May 9, 2026 20:29
This series layers an HD-rendering / automation feature set on top of the
upstream hode engine without altering the original game logic.

New modules:
- hd_compositor: HD framebuffer, multi-resolution presets (HD/FullHD/QHD/4K),
  16:9 widescreen with palette-derived gradient borders, sprite/background
  composition, prerender driver + unified console progress bar.
- sprite_upscaler: chained xBRZ scalers (2x..5x with 4x via 2x*2x and 5x via
  nearest), MLAA edge smoothing, RAM LRU + disk cache keyed by content hash
  (sprite bytes + dims + flip flags + palette hash) so cached frames remain
  correct across screen palette changes and persist across runs.
- edge_smooth: MLAA anti-aliasing applied to upscaled output.
- automation_api: Unix-domain socket JSON server (input injection, frame
  step, screenshot, get_state, set_level) for scripted/headless testing.
- test_*.py: Python regression scripts driving the automation API.

Engine integration:
- main.cpp: --hd, --fullhd, --4k, --hd-scale=N, --hd-wide, --hd-cache=PATH,
  --smooth, --automation=PATH, --prerender flags.
- game.cpp: HD compositor begin/end-frame hooks in drawScreen, smooth-anim
  decoupled render loop (60 Hz render, 12.5 Hz logic), prerender phase that
  walks every sprite type x frame x flip variant plus the level's small
  in-gameplay PAF clips and intro cutscene, with one shared progress bar.
- paf.cpp: HD frame callback path, _prerenderMode that decodes every frame
  full-speed without audio/display/sleep so the disk cache populates,
  peekFramesCount helper for sizing the unified progress bar.
- video.cpp: zero-init _font in ctor.

Menu and input fixes:
- menu.cpp/h: keyboard binding screen learns OK / Cancel / Test sub-buttons,
  binding a key clears the same scancode from any other action (one key per
  action), Space/Enter/Tab/Backspace/Win can now be bound, fallback text
  labels (SP/EN/TB/BS/WN) for keys without an icon glyph.
- system_sdl2.cpp: applyKeyboardControls() now keeps the original default
  mappings active and adds user keys on top so binding one action no longer
  silences the others. waitForKeyPress clears edge state on return so the
  user's previously held SELECT key doesn't re-fire on the next event tick.
- main.cpp: wire _video->_font from _res->_fontBuffer right after
  loadSetupDat(), since the menu (which runs before Game::mainLoop) draws
  text via _video->drawStringCharacter.

Portability:
- intern.h: macOS branch using <libkern/OSByteOrder.h> (macOS doesn't ship
  glibc-style <endian.h>).
Replaces the brief README with a structured reference covering:

- macOS / Linux / Windows-WSL build instructions (incl. macOS-specific
  notes on the libkern/OSByteOrder.h shim, Homebrew SDL2 setup,
  Gatekeeper, headless mode caveats).
- Full CLI flag reference table (--hd, --fullhd, --4k, --hd-scale,
  --hd-wide, --hd-cache, --prerender, --smooth, --automation, --level,
  --checkpoint, --datapath, --savepath, --debug, --cheats).
- Full hode.ini reference for both [engine] and [display] sections.
- Display modes deep-dive: standard, HD upscaling, scale-chain table
  for every supported factor, widescreen 16:9 borders, smooth 60 Hz
  interpolated render.
- Disk cache and prerender flow: on-disk layout, cache key formula
  (FNV-1a over decoded SPR bytes + dims + flags + palette hash), why
  upstream's pointer-keyed cache never produced cross-run hits.
- Automation API: protocol, every JSON command (get_state, input,
  step, screenshot, set_level), reply schema, input bit layout,
  Python client example, headless testing, bundled test scripts.
- Engine architecture: high-level dataflow diagram, core types,
  HD pipeline, PAF cutscene flow.
- Source-file map: original engine files, files added by this fork,
  files modified vs upstream with the why.
- Game world reference: levels, all 25 cutscenes, default key bindings,
  settings menu (incl. OK/Cancel/Test, one-key-one-action, fallback
  text labels for Space/Enter/Tab/Backspace/Win), cheat flag bitmask,
  debug bitmask.
- Data formats brief: LVL/SSS/MST/PAF/SETUP/setup.cfg structure.
- Troubleshooting, differences from upstream, known limitations,
  contributing notes, credits, license/legal.
- Promotes README.md to the canonical README and removes the bare
  README.txt (GitHub already prefers .md, but having two is confusing
  and README.txt was the upstream plain-text version).
- Adds badges, a centered hero block, a 2-column highlight matrix,
  GitHub-flavoured admonition callouts (NOTE/TIP/IMPORTANT/CAUTION/
  WARNING), collapsible <details> sections for the long lists
  (macOS notes, source-file map, cutscene table, data formats), and
  a styled footer.
- Restructures with emoji-prefixed sections so the GitHub TOC reads
  like a proper docs landing page.
- Tightens the macOS build section (Apple Silicon vs Intel, brew
  PATH for /opt/homebrew, lldb debug-build recipe, headless caveats).
- Standardises code block language hints and table alignment.
@CommonLoon102
Copy link
Copy Markdown

I suggest you add this to your AI's context:
https://articles.mergify.com/pull-request-best-practices-complete-guide-developers/

Then you can open 50 smaller PRs instead of this one, no-one is going to review this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants