Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
PAGES_DOOM_IWAD: .tools/assets/freedoom-0.13.0.zip
PAGES_DOOM_CROM_FILE_BYTES: "2097152"
PAGES_DOOM_CROM_FILE_BYTES: "4194304"

concurrency:
group: pages-${{ github.ref }}
Expand Down Expand Up @@ -258,12 +258,12 @@ jobs:
import zipfile

expected_rom = {
"202-p1.p1": 0x2B61415B,
"202-s1.s1": 0xCD19264F,
"202-c1.c1": 0xCC0095EF,
"202-c2.c2": 0x42371307,
"202-m1.m1": 0x9C0291EA,
"202-v1.v1": 0xDEBEB8FB,
"221-p1.p1": 0x7BE82353,
"221-s1.s1": 0x2A4063A3,
"221-c1.c1": 0x1F862A14,
"221-c2.c2": 0x14B90536,
"221-m1.m1": 0xBDDAE628,
"221-v1.v1": 0x7E5E53E4,
}
expected_bios = {
"sp-s3.sp1": 0x91B64BE3,
Expand All @@ -281,12 +281,23 @@ jobs:
raise SystemExit(f"{path}:{name} CRC {actual:08x} != {crc:08x}")

main_rom = glob.glob("dist/pages/rom/web-*/doomgeo-aes.zip")
main_launch_rom = glob.glob("dist/pages/rom/web-*/magdrop2.zip")
main_bios = glob.glob("dist/pages/rom/web-*/neogeo.zip")
asm_rom = glob.glob("dist/pages/rom/asm/web-*/doomgeo-aes-asm.zip")
if len(main_rom) != 1 or len(main_bios) != 1 or len(asm_rom) != 1:
raise SystemExit(f"unexpected web package layout: {main_rom=} {main_bios=} {asm_rom=}")
asm_launch_rom = glob.glob("dist/pages/rom/asm/web-*/magdrop2.zip")
if len(main_rom) != 1 or len(main_launch_rom) != 1 or len(main_bios) != 1 or len(asm_rom) != 1 or len(asm_launch_rom) != 1:
raise SystemExit(
"unexpected web package layout: "
f"{main_rom=} {main_launch_rom=} {main_bios=} {asm_rom=} {asm_launch_rom=}"
)
check(main_rom[0], expected_rom)
check(main_rom[0], expected_bios)
check(main_launch_rom[0], expected_rom)
check(main_launch_rom[0], expected_bios)
check(asm_rom[0], expected_rom)
check(asm_rom[0], expected_bios)
check(asm_launch_rom[0], expected_rom)
check(asm_launch_rom[0], expected_bios)
check(main_bios[0], expected_bios)
PY

Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,8 @@ gngeo gngeo-fullscreen: GNGEO_DATAFILE=$(GNGEO_RUN_DATAFILE)
gngeo gngeo-fullscreen: $(GNGEO_RUN_DATAFILE)

# Keep the packaged C-ROM size explicit. Local GnGeo launches generate a
# matching datafile because the stock puzzledp metadata only maps 1 MiB per
# C-ROM and corrupts/omits larger generated sprite banks.
# matching datafile because stock arcade metadata can map smaller C-ROM windows
# than the generated sprite bank.
CROMSIZE=$(DOOM_CROM_FILE_BYTES)


Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,9 @@ python3 tools/inspect_map_specials.py --map E1M2

You must provide your own Neo Geo BIOS for local emulation. The browser package
uses a separate Freedoom-based ROM and an FBNeo-compatible packaging path, but
that does not rename the project: `DoomGeo-AES` is the game name; `puzzledp` is
only the private FBNeo driver/chip identity used so the arcade core accepts the
generated homebrew ROM zip.
that does not rename the project: `DoomGeo-AES` is the game name. The browser
player launches a `magdrop2` romset package internally so the arcade core
accepts the generated homebrew ROM zip.

## Documentation

Expand Down
23 changes: 15 additions & 8 deletions docs/build-packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@ expose the standalone 68000 assembly ROM at `asm.html`.

FBNeo is an arcade core and validates known romsets by filename, size, and CRC.
For the Pages bundle only, `doomgeo-build pages` writes FBNeo-compatible launch
zips against the `puzzledp` driver by adjusting unused padding bytes in the
generated ROM chips and null BIOS aliases. The public Pages/download zip names
are still `doomgeo-aes.zip` and `doomgeo-aes-asm.zip`; `puzzledp` remains only
the private FBNeo driver/chip compatibility identity. The raw build artifacts
under `build/rom/` are not rewritten this way; this compatibility package is
only for the browser player.
zips against the `magdrop2` driver by adjusting unused padding bytes in the
generated ROM chips and null BIOS aliases. `magdrop2` is used because its FBNeo
driver has 4 MiB C1/C2 graphics ROM windows, matching the generated web sprite
bank without cutting assets back to a smaller romset shape. The browser
player launches `magdrop2.zip` and sets `EJS_gameName = "magdrop2"` because
FBNeo identifies arcade games by driver/romset name. The bundle also writes
project-named copies
(`doomgeo-aes.zip` and `doomgeo-aes-asm.zip`) for download links, but those are
not the files passed to the emulator. The raw build artifacts under
`build/rom/` are not rewritten this way; this compatibility package is only for
the browser player.

The deployed Pages ROM is built separately from the normal native artifact. CI
uses the redistributable Freedoom IWAD and `DOOM_CROM_FILE_BYTES=2097152` for
uses the redistributable Freedoom IWAD and `DOOM_CROM_FILE_BYTES=4194304` for
that job so the browser package does not embed proprietary Doom assets and still
matches the fixed C-ROM chip sizes expected by FBNeo's `puzzledp` driver. The
matches the fixed C-ROM chip sizes expected by FBNeo's `magdrop2` driver. The
regular Linux, Windows, and local ROM artifacts are left on the normal build
path.

Expand Down Expand Up @@ -395,8 +400,10 @@ The Pages job consumes the Freedoom web ROM artifact and writes:
```text
dist/pages/index.html
dist/pages/asm.html
dist/pages/rom/web-<hash>/magdrop2.zip
dist/pages/rom/web-<hash>/doomgeo-aes.zip
dist/pages/rom/web-<hash>/neogeo.zip
dist/pages/rom/asm/web-<hash>/magdrop2.zip
dist/pages/rom/asm/web-<hash>/doomgeo-aes-asm.zip
```

Expand Down
5 changes: 3 additions & 2 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -890,5 +890,6 @@ readable.
- Windows builds are supported through MSYS2 UCRT64.
- GitHub Actions builds Linux ROM, Windows ROM, standalone helper binaries, a
Pages-playable FBNeo package, and a separate 68000 ASM ROM.
- Browser packages are public `doomgeo-aes.zip` / `doomgeo-aes-asm.zip` files,
while the internal FBNeo chip names/CRCs match the `puzzledp` driver.
- Browser packages launch `magdrop2.zip` with internal FBNeo chip names/CRCs
matching the `magdrop2` driver, with project-named copies available as
download links.
11 changes: 6 additions & 5 deletions docs/release-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,23 @@ the other only accompanies and tracks the plan.

## Evidence

- Linux ROM builds are expected to produce `build/rom/puzzledp.zip` internally,
- Linux ROM builds are expected to produce `build/rom/magdrop2.zip` internally,
then package it as `dist/rom/doomgeo-aes.zip`.
- ASM ROM builds are expected to produce `build/asm-rom/puzzledp.zip` from
- ASM ROM builds are expected to produce `build/asm-rom/magdrop2.zip` from
`asm/doomgeo_asm.S`, then package it as `dist/asm-rom/doomgeo-aes-asm.zip`.
- Windows/MSYS2 ROM builds are expected to produce the same packaged
`doomgeo-aes.zip` through the UCRT64 ngdevkit packages.
- Standalone helper builds are expected to produce `doomgeo-build` and
`doomgeo-plan` artifacts for Linux, plus `.exe` variants for Windows.
- The Pages bundle is expected to publish `index.html`, `asm.html`,
`rom/web-<hash>/doomgeo-aes.zip`, `rom/web-<hash>/neogeo.zip`, and
`rom/web-<hash>/magdrop2.zip`, `rom/web-<hash>/doomgeo-aes.zip`,
`rom/web-<hash>/neogeo.zip`, `rom/asm/web-<hash>/magdrop2.zip`, and
`rom/asm/web-<hash>/doomgeo-aes-asm.zip`.
- The Pages ROM zips are expected to be FBNeo-compatible launch packages with
`puzzledp` driver chip filenames, sizes, and CRCs internally while preserving
`magdrop2` driver chip filenames, sizes, and CRCs internally while preserving
the generated homebrew data outside the final padding correction bytes.
- The Pages main ROM is expected to come from a separate Freedoom-based build
with 2 MiB C-ROM chips, not from the normal native/local ROM artifact.
with 4 MiB C-ROM chips, not from the normal native/local ROM artifact.
- Repo-local installs are removable with `doomgeo-build uninstall`; `--all`
also removes cached WAD/package downloads under `.tools`.
- The installer decision is MSYS2 UCRT64 for native Windows builds, with WSL
Expand Down
2 changes: 1 addition & 1 deletion rom.mk
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ BUILDDIR?=build
ROM?=$(BUILDDIR)/rom

# ROM name
GAMEROM=puzzledp
GAMEROM=magdrop2

# program ROM
PROMSIZE=524288
Expand Down
113 changes: 64 additions & 49 deletions tools/doomgeo_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,21 @@
PROJECT_NAME = "DoomGeo-AES"
PACKAGE_ROM_ZIP = "doomgeo-aes.zip"
PACKAGE_ASM_ROM_ZIP = "doomgeo-aes-asm.zip"
FBNEO_COMPAT_ROM_ZIP = "puzzledp.zip"
FBNEO_COMPAT_ROM_ZIP = "magdrop2.zip"
FBNEO_DRIVER_NAME = "magdrop2"
ROM_ZIP = Path("build") / "rom" / FBNEO_COMPAT_ROM_ZIP
ROM_ELF = Path("build") / "rom.elf"
BIOS_ZIP = Path("build") / "rom" / "neogeo.zip"
ASM_ROM_ZIP = Path("build") / "asm-rom" / FBNEO_COMPAT_ROM_ZIP
ASM_ROM_ELF = Path("build") / "asm" / "doomgeo_asm.elf"
ASM_BIOS_ZIP = Path("build") / "asm-rom" / "neogeo.zip"
FBNEO_PUZZLEDP_CRC = {
"202-p1.p1": 0x2B61415B,
"202-s1.s1": 0xCD19264F,
"202-c1.c1": 0xCC0095EF,
"202-c2.c2": 0x42371307,
"202-m1.m1": 0x9C0291EA,
"202-v1.v1": 0xDEBEB8FB,
}
FBNEO_PUZZLEDP_SIZE = {
"202-p1.p1": 0x80000,
"202-s1.s1": 0x20000,
"202-c1.c1": 0x200000,
"202-c2.c2": 0x200000,
"202-m1.m1": 0x20000,
"202-v1.v1": 0x80000,
FBNEO_ROM_ENTRIES = {
"221-p1.p1": ("202-p1.p1", 0x80000, 0x7BE82353),
"221-s1.s1": ("202-s1.s1", 0x20000, 0x2A4063A3),
"221-c1.c1": ("202-c1.c1", 0x400000, 0x1F862A14),
"221-c2.c2": ("202-c2.c2", 0x400000, 0x14B90536),
"221-m1.m1": ("202-m1.m1", 0x20000, 0xBDDAE628),
"221-v1.v1": ("202-v1.v1", 0x200000, 0x7E5E53E4),
}
FBNEO_NEOGEO_CRC = {
"sp-s3.sp1": 0x91B64BE3,
Expand Down Expand Up @@ -364,11 +357,13 @@ def force_crc32(data: bytes, desired: int, patch_offset: int | None = None) -> b
return bytes(patched)


def write_zip_entries(path: Path, entries: dict[str, bytes]) -> None:
def write_zip_entries(path: Path, entries: dict[str, bytes], compression: int | None = None) -> None:
import zipfile

path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
if compression is None:
compression = zipfile.ZIP_DEFLATED
with zipfile.ZipFile(path, "w", compression=compression) as archive:
for name, data in entries.items():
archive.writestr(name, data)

Expand All @@ -381,35 +376,20 @@ def web_asset_version(paths: list[Path]) -> str:
return digest.hexdigest()[:12]


def build_fbneo_rom_zip(source_zip: Path, out_zip: Path) -> None:
import zipfile

if not source_zip.exists():
raise BuildError(f"ROM not found: {source_zip}")
entries: dict[str, bytes] = {}
with zipfile.ZipFile(source_zip) as archive:
for name, desired_crc in FBNEO_PUZZLEDP_CRC.items():
try:
data = archive.read(name)
except KeyError as exc:
raise BuildError(f"ROM entry missing for FBNeo package: {name}") from exc
expected_size = FBNEO_PUZZLEDP_SIZE[name]
if len(data) != expected_size:
raise BuildError(
f"ROM entry {name} is {len(data)} bytes, but the FBNeo puzzledp "
f"web driver expects {expected_size} bytes; rebuild the Pages ROM "
"with DOOM_CROM_FILE_BYTES=2097152"
)
entries[name] = force_crc32(data, desired_crc)
write_zip_entries(out_zip, entries)
print_step(f"wrote FBNeo-compatible ROM package to {out_zip}")
def pad_rom_entry(data: bytes, expected_size: int, name: str) -> bytes:
if len(data) > expected_size:
raise BuildError(
f"ROM entry {name} is {len(data)} bytes, but the FBNeo {FBNEO_DRIVER_NAME} "
f"web driver expects {expected_size} bytes"
)
if len(data) < expected_size:
return data + (b"\xff" * (expected_size - len(data)))
return data


def build_fbneo_bios_zip(source_zip: Path, out_zip: Path) -> None:
def fbneo_bios_entries(source_zip: Path) -> dict[str, bytes]:
import zipfile

if not source_zip.exists():
raise BuildError(f"BIOS not found: {source_zip}")
with zipfile.ZipFile(source_zip) as archive:
source_names = set(archive.namelist())

Expand All @@ -425,13 +405,41 @@ def read_first(*names: str) -> bytes:
"sfix.sfix": force_crc32(read_first("sfix.sfix"), FBNEO_NEOGEO_CRC["sfix.sfix"]),
"000-lo.lo": force_crc32(read_first("000-lo.lo"), FBNEO_NEOGEO_CRC["000-lo.lo"]),
}
return entries


def build_fbneo_rom_zip(source_zip: Path, out_zip: Path, bios_zip: Path | None = None) -> None:
import zipfile

if not source_zip.exists():
raise BuildError(f"ROM not found: {source_zip}")
entries: dict[str, bytes] = {}
with zipfile.ZipFile(source_zip) as archive:
for target_name, (source_name, expected_size, desired_crc) in FBNEO_ROM_ENTRIES.items():
try:
data = archive.read(source_name)
except KeyError as exc:
raise BuildError(f"ROM entry missing for FBNeo package: {source_name}") from exc
data = pad_rom_entry(data, expected_size, target_name)
entries[target_name] = force_crc32(data, desired_crc)
if bios_zip:
entries.update(fbneo_bios_entries(bios_zip))
write_zip_entries(out_zip, entries, compression=zipfile.ZIP_STORED)
print_step(f"wrote FBNeo-compatible ROM package to {out_zip}")


def build_fbneo_bios_zip(source_zip: Path, out_zip: Path) -> None:
if not source_zip.exists():
raise BuildError(f"BIOS not found: {source_zip}")
entries = fbneo_bios_entries(source_zip)
write_zip_entries(out_zip, entries)
print_step(f"wrote FBNeo-compatible BIOS package to {out_zip}")


def html_page(
game_name: str,
subtitle: str,
emulator_game_name: str,
game_url: str,
download_url: str,
bios_url: str,
Expand Down Expand Up @@ -530,7 +538,7 @@ def html_page(
<script>
window.EJS_player = "#game";
window.EJS_core = "fbneo";
window.EJS_gameName = "__GAME_NAME__";
window.EJS_gameName = "__EMULATOR_GAME_NAME__";
window.EJS_gameUrl = "__GAME_URL__";
window.EJS_biosUrl = "__BIOS_URL__";
window.EJS_pathtodata = "https://cdn.emulatorjs.org/stable/data/";
Expand All @@ -542,6 +550,7 @@ def html_page(
"""
return (
template.replace("__GAME_NAME__", game_name)
.replace("__EMULATOR_GAME_NAME__", emulator_game_name)
.replace("__SUBTITLE__", subtitle)
.replace("__GAME_URL__", game_url)
.replace("__DOWNLOAD_URL__", download_url)
Expand Down Expand Up @@ -570,16 +579,19 @@ def build_pages(
rom_out = out_dir / "rom" / f"web-{version}"
rom_out.mkdir(parents=True, exist_ok=True)
main_rom_url = f"rom/web-{version}/{PACKAGE_ROM_ZIP}"
main_launch_rom_url = f"rom/web-{version}/{FBNEO_COMPAT_ROM_ZIP}"
bios_url = f"rom/web-{version}/neogeo.zip"
build_fbneo_rom_zip(rom, rom_out / PACKAGE_ROM_ZIP)
build_fbneo_rom_zip(rom, rom_out / FBNEO_COMPAT_ROM_ZIP, bios)
shutil.copy2(rom_out / FBNEO_COMPAT_ROM_ZIP, rom_out / PACKAGE_ROM_ZIP)
build_fbneo_bios_zip(bios, rom_out / "neogeo.zip")
# FBNeo still matches the internal Neo Geo chip filenames and CRCs against
# the Puzzle De Pon driver. The public zip name can be project-specific.
# a known driver. The public page title and download copy stay project-specific.
(out_dir / "index.html").write_text(
html_page(
PROJECT_NAME,
"Neo Geo AES Doom prototype running in a browser through the EmulatorJS FBNeo WebAssembly core.",
main_rom_url,
FBNEO_DRIVER_NAME,
main_launch_rom_url,
main_rom_url,
bios_url,
"asm.html" if asm_rom_source else None,
Expand All @@ -593,12 +605,15 @@ def build_pages(
asm_out = out_dir / "rom" / "asm" / f"web-{version}"
asm_out.mkdir(parents=True, exist_ok=True)
asm_rom_url = f"rom/asm/web-{version}/{PACKAGE_ASM_ROM_ZIP}"
build_fbneo_rom_zip(asm_rom, asm_out / PACKAGE_ASM_ROM_ZIP)
asm_launch_rom_url = f"rom/asm/web-{version}/{FBNEO_COMPAT_ROM_ZIP}"
build_fbneo_rom_zip(asm_rom, asm_out / FBNEO_COMPAT_ROM_ZIP, bios)
shutil.copy2(asm_out / FBNEO_COMPAT_ROM_ZIP, asm_out / PACKAGE_ASM_ROM_ZIP)
(out_dir / "asm.html").write_text(
html_page(
f"{PROJECT_NAME} ASM",
"A separate 68000 assembly cartridge build with a controller-driven Neo Geo sprite scene.",
asm_rom_url,
FBNEO_DRIVER_NAME,
asm_launch_rom_url,
asm_rom_url,
bios_url,
),
Expand Down
8 changes: 4 additions & 4 deletions tools/simple_enemy_visibility_probe.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
"""Probe the authored simple-map live enemy visibility path.

The texture-column experiment uses generated Doom sprites inside GnGeo's
puzzledp C-ROM window. A full sprite bank can push live monster tiles past the
visible C-ROM range while the earlier corpse frames still fit, producing an
invisible live monster and a briefly visible death frame.
The texture-column experiment uses generated Doom sprites inside the configured
Neo Geo C-ROM window. A mismatched romset/datafile can push live monster tiles
past the visible C-ROM range while the earlier corpse frames still fit,
producing an invisible live monster and a briefly visible death frame.
"""

from __future__ import annotations
Expand Down
3 changes: 2 additions & 1 deletion tools/smoke_capture.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ WAIT_SECS="${SMOKE_WAIT_SECS:-8}"
START_GAME="${SMOKE_START_GAME:-0}"
EXTRAOPTS_VALUE="${SMOKE_EXTRAOPTS:-}"
DIRECT_ROM="${SMOKE_DIRECT_ROM:-}"
DIRECT_ROMSET="${SMOKE_DIRECT_ROMSET:-magdrop2}"
CAPTURE_P1CONTROL="${SMOKE_P1CONTROL:-A=K122,B=K120,C=K97,D=K115,START=K49,COIN=K51,UP=K82,DOWN=K81,LEFT=K80,RIGHT=K79}"
OUT="${SMOKE_OUTPUT:-.tools/screens/latest/smoke.png}"
LOG="${SMOKE_LOG:-.tools/logs/smoke-gngeo.log}"
Expand Down Expand Up @@ -184,7 +185,7 @@ if [ -n "$DIRECT_ROM" ]; then
"$ROOT/.tools/ngdevkit-local/usr/bin/ngdevkit-gngeo" \
--datafile="$ROOT/.tools/ngdevkit-local/usr/share/ngdevkit-gngeo/gngeo_data.zip" \
--p1control="$CAPTURE_P1CONTROL" \
$EXTRAOPTS_VALUE --screen320 --scale 3 --no-resize -i "$DIRECT_ROM" puzzledp >"$LOG" 2>&1 < /dev/null &
$EXTRAOPTS_VALUE --screen320 --scale 3 --no-resize -i "$DIRECT_ROM" "$DIRECT_ROMSET" >"$LOG" 2>&1 < /dev/null &
RUN_PID="$!"
else
run_args=("$RUN_TARGET")
Expand Down
Loading