# Building the Gearcoleco dual VRAM cores from corresponding source

This document is the "scripts used to control compilation" part of the GPL corresponding
source for the two `.data` archives in `package/`. Everything named here is pinned by hash
in `release-manifest.json` and in `build-contract/.github/legacy-wasm/pinned-inputs.json`.

## What is being built

Two EmulatorJS-format browser cores for Gearcoleco (ColecoVision):

| Variant | Archive | WebGL profile | wrapper args |
| --- | --- | --- | --- |
| regular | `gearcoleco-wasm.data` | `FULL_ES3=1`, `MIN_WEBGL_VERSION=2`, `MAX_WEBGL_VERSION=2` | `--clean` |
| legacy  | `gearcoleco-legacy-wasm.data` | `FULL_ES2=1`, `MIN_WEBGL_VERSION=1`, `MAX_WEBGL_VERSION=2` | `--clean --legacy` |

Both wrapper links consume **one** shared Gearcoleco core archive
(`gearcoleco_libretro_emscripten.bc`, sha256
`a453580ae0248b58b5df5b78ebb0e8a98edb4637fec7f0c0789bd2925ac98673`), so the emulator code in
both variants is identical; only the RetroArch/WebGL wrapper differs. Each final link pulls in
260 object files; the exact component map is in `THIRD_PARTY_NOTICES.txt`.

## Host requirements

- Linux x86-64 (the reference build runs on GitHub `ubuntu-24.04`)
- `docker` able to run `linux/amd64` images
- `git`, `curl`, `python3`, `bash`
- Outbound HTTPS to `github.com`, `codeload.github.com` and `cdn.emulatorjs.org`

The compiler is never installed on the host. All compilation happens inside a digest-pinned
container:

```
emscripten/emsdk@sha256:92c97951b9a6835cb5da9592e9d95226f67e09ecd01a541d817a5b4801f235a4
```

emsdk 4.0.8 / Emscripten `70404efec4458b60b953bc8f1529f2fa112cdfd1` / LLVM
`23e3cbb2e82b62586266116c8ab77ce68e412cf8` / Binaryen
`efb987b9ba6a8fdad8dac2aea2c46dce398ce55d` (version 123), Node 20.18.0 at
`/emsdk/node/20.18.0_64bit/bin/node`. Its registry manifest's config digest is
`sha256:2a003481ef771a04cc1a7b5c4be03db0b3cb76405aaddac0d27629a5aac322c2`.

> On Docker installations backed by the containerd image store, `docker image inspect` reports
> `.Id` as the **manifest** digest rather than the config digest. That is a reporting
> difference, not a content mismatch — the image is pulled by its content-addressed manifest
> digest. Cross-check the config digest with `docker manifest inspect` if you need it.

The build container runs with **`--network=none`**; every input is staged beforehand.

## Reproducing the reference build

1. Check out `gearcoleco-patched` at commit `86ec2f7e6feda60a8968f49a13d93c5a57682f9e`.
   The contents of `build-contract/` are byte-identical to that commit — all 27 files were
   compared blob-for-blob against it, and the 26 files pinned by `contract_files` match their
   recorded sizes and SHA-256 values.

2. Run the dual contract exactly as CI does:

   ```sh
   LEGACY_CONTRACT=dual \
   LEGACY_WORK_ROOT=/some/empty/dir \
     bash --noprofile --norc .github/legacy-wasm/rebuild.sh
   ```

3. `rebuild.sh` validates the contract statically, verifies every pin, fetches the four pinned
   upstream revisions by exact commit id and checks their tree ids, downloads and hash-checks
   the two upstream baselines from `https://cdn.emulatorjs.org/4.2.3/data/cores/`, seeds an
   offline zlib 1.3.1 port cache, builds the shared core once, links the legacy and regular
   wrappers, packs each into a 5-member 7-Zip `.data` archive, emits `provenance.json`, and
   re-verifies both archives read-only.

4. Output lands in `$LEGACY_WORK_ROOT/output/dual/` as exactly six files: `core-build.log`,
   `gearcoleco-legacy-wasm.data`, `gearcoleco-wasm.data`, `provenance.json`,
   `wrapper-legacy.log`, `wrapper-regular.log`.

## Building from the tarballs in `corresponding-source/`

The archives are produced with `git archive` under `core.autocrlf=false` and `core.eol=lf`, so
they carry upstream bytes exactly. Each was audited entry-by-entry against the pinned upstream
tree with `git hash-object --no-filters` — zero mismatches, symlinks included.

To verify a tarball yourself:

```sh
tar -xzf corresponding-source/gearcoleco-f80af5d6f0f7.tar.gz
cd gearcoleco-f80af5d6f0f7
git init -q . && git add -A && git write-tree
# -> 84c369a3f39e45ee1192eecfd4b5e017edbca5cc, the pinned Gearcoleco tree
```

> This `write-tree` shortcut works for Gearcoleco, EmulatorJS and the build recipe, which have
> no in-tree `.gitattributes`. It does **not** work for RetroArch: `deps/glslang/glslang` and
> `deps/xxHash` carry `.gitattributes` with `text`/`eol` rules, so `git add` renormalises and
> the reconstructed tree id differs. For RetroArch (and Emscripten), compare per entry with
> `git hash-object --no-filters` against `git ls-tree -r`, which is what this package's audit
> does.

### Submodules

Gearcoleco, RetroArch, EmulatorJS and the build recipe have **no submodules**, so their
tarballs are complete.

Emscripten has exactly three, all under `test/third_party/` and therefore absent from the
tarball:

| Submodule | Commit | Upstream |
| --- | --- | --- |
| `test/third_party/googletest` | `b514bdc898e2951020cbdca1304b75f5950d1f59` | https://github.com/google/googletest |
| `test/third_party/posixtestsuite` | `869e266b090a8d44e51bb040904c942556b1e4ab` | https://github.com/emscripten-core/posixtestsuite |
| `test/third_party/wasi-test-suite` | `ac9371207fc61972a21d7d086074ca690a99d14d` | https://github.com/khronosproject/wasi-test-suite |

None is under `system/lib/` or `src/`; they are Emscripten's own test suites and contribute no
code to the runtime, libc or ports that are linked into the distributed binaries. Their exact
commits are listed here so they can be retrieved if anyone wants to run Emscripten's test
suite.

### Applying the patch stack by hand

Apply the three patches in `build-contract/.github/legacy-wasm/patches/` in this order. Each
one's expected input and output git blob ids are in `release-manifest.json` under `patch_stack`.

```sh
# Gearcoleco @ f80af5d6f0f78b0228e060e158e7f2500e0caa6d (tree 84c369a3f39e45ee1192eecfd4b5e017edbca5cc)
#   platforms/libretro/libretro.cpp  364b28efc831672911f546297842aff2b936cba5 ->
git apply --whitespace=error-all f80af5d-pooyan-input-resync.patch   # -> 76ba35c9946f8667a895869608e6976c7606eab2
git apply --whitespace=error-all f80af5d-vram-export.patch           # -> 3dec679c21aee6489161d3617174a33b67c1b2c5

# RetroArch @ 6dd4353937ef48b6ec0bfbdbb15d1c5992d86927 (tree 679ee43607704047b4feab5cb13d5129c785782f)
#   Makefile.emulatorjs  fbf6fa57c00d7ce882353b3530278a0d55466f04 ->
git apply --unidiff-zero --whitespace=error-all 6dd4353-makefile-vram-exports.patch  # -> e4adfc95179fe76316b853c46047ff65dff4c9a1
```

`6dd4353-makefile-vram-exports.patch` is a single zero-context hunk, so `git apply` **requires**
`--unidiff-zero`; without it the patch is refused. This mirrors
`build-contract/.github/legacy-wasm/vram-dual/build-dual.sh` line 72.

## What the patches do

- `f80af5d-pooyan-input-resync.patch` — a previously reviewed input-resynchronisation backport
  to `platforms/libretro/libretro.cpp` (+71/-20). Not VRAM related; part of the pinned baseline.
- `f80af5d-vram-export.patch` — adds a `case RETRO_MEMORY_VIDEO_RAM:` arm to each of
  `retro_get_memory_data()` and `retro_get_memory_size()` (+4 lines over two hunks), returning
  the TMS9918A VRAM pointer and its 0x4000-byte length.
- `6dd4353-makefile-vram-exports.patch` — appends `_retro_get_memory_data,_retro_get_memory_size`
  to `EXPORTED_FUNCTIONS` in RetroArch's `Makefile.emulatorjs` (1 line), so the two symbols
  survive into the Emscripten glue.

See `MODIFICATIONS.txt` for the GPL modification notice and dates, and `PATCHES-LICENSE.txt`
for the patches' own licence grant.

## Toolchain source

`corresponding-source/emscripten-70404efec445.tar.gz` is the Emscripten tree at the exact pinned
commit (tree `67e39378a828bd6dd330f871133de3f51e7c9c9c`). It carries every system library the
final link pulls from the sysroot:

| System library | Upstream path in that tree | Files | SPDX | Linked |
| --- | --- | ---: | --- | --- |
| `libc` (musl) | `system/lib/libc/musl` | 1 628 | `MIT` | yes |
| `libc++-noexcept` | `system/lib/libcxx` | 1 134 | `Apache-2.0 WITH LLVM-exception` | yes |
| `libc++abi-noexcept` | `system/lib/libcxxabi` | 42 | `Apache-2.0 WITH LLVM-exception` | yes |
| `libcompiler_rt` | `system/lib/compiler-rt` | 547 | `Apache-2.0 WITH LLVM-exception` | yes |
| `libdlmalloc` | `system/lib/dlmalloc.c` | 1 | `CC0-1.0` | yes — the selected allocator |
| `libunwind` | `system/lib/libunwind` | 35 | `Apache-2.0 WITH LLVM-exception` | **no** |
| `emmalloc` | `system/lib/emmalloc.c` | 1 | `MIT OR NCSA` | **no** |

The linked set was read from the toolchain's own `generating system library:` records and is
identical in the CI run and in the independent validation build that started with an empty
`EM_CACHE`. `libunwind` is absent from every log — the `-noexcept` variants of libc++ and
libc++abi show exceptions are disabled — and `emmalloc` is not linked because `libdlmalloc` is.

Each of libc++, libc++abi and compiler-rt ships its own upstream `LICENSE.TXT` verbatim as
`licenses/llvm-libcxx-LICENSE.TXT`, `licenses/llvm-libcxxabi-LICENSE.TXT` and
`licenses/llvm-compiler-rt-LICENSE.TXT` — three separate files, not one shared LLVM licence.
dlmalloc's upstream dedication is quoted verbatim in `licenses/dlmalloc-NOTICE.txt`.

The zlib port is built by Emscripten's own recipe `tools/ports/zlib.py` in that tree. There are
**no** port patches — `tools/ports/zlib.py` is the only `ports/zlib*` path in the tree — so the
zlib sources compiled in are exactly `corresponding-source/zlib-1.3.1.tar.gz`
(`17e88863f3600672ab49182f217281b6fc4d3c762bde361935e436a95214d05c`, the pinned upstream release
tarball), unmodified.

## Independent local validation build

This package was validated by rebuilding from these staged tarballs alone, inside the pinned
container with `--network=none`. Results are in `validation-build/`.

**All ten archive members of both variants came out bit-for-bit identical to the release** —
the shared core `gearcoleco_libretro_emscripten.bc`
(`a453580ae0248b58b5df5b78ebb0e8a98edb4637fec7f0c0789bd2925ac98673`), both
`gearcoleco_libretro.wasm`, both `gearcoleco_libretro.js`, and the three metadata members. The
public export surface matched down to the same minified aliases.

The two outer `.data` containers differ from the released ones by exactly **18 bytes** each:
identical size, member order, method, block layout and per-member CRC32, but different 7z header
CRC fields because three metadata members carried different POSIX permission bits in the
validation staging. See `validation-build/README.md` for the full log, the byte-level analysis,
and the exact network dependencies used to stage the inputs.

## Verifying this package against the release

```sh
sha256sum package/gearcoleco-wasm.data          # 94a8ea34717e87c85f9823d7cdf3101c510c2d9eeef7cc802835ba6996b48af8
sha256sum package/gearcoleco-legacy-wasm.data   # a8dab431de40d5c9e6c278dc97de0cd5160fb08e6540b27e9b94d2bf4fc890e4
sha256sum actions-artifact/gearcoleco-dual-wasm-candidate-86ec2f7e.zip
                                                # 9558ebacbc3e426531095597745dee024010cc57fb0b771dce9d7e1122f1209d
sha256sum -c SHA256SUMS
```

Every compiled artefact reproduces bit-for-bit from this corresponding source (see above), but
full-archive reproducibility of the `.data` containers has **not** been demonstrated: the
validation rebuild's containers differ by 18 header bytes traceable to stored POSIX permission
bits. Treat the `.data` hashes as the identity of the CI build, not as a claimed deterministic
container output.

## Consuming the VRAM export

Both variants export `_retro_get_memory_data` and `_retro_get_memory_size` as callable
WebAssembly functions reachable as `Module["_retro_get_memory_data"]` /
`Module["_retro_get_memory_size"]`. Call them with the libretro id `3`
(`RETRO_MEMORY_VIDEO_RAM`); the size call returns `0x4000` once a core is loaded. Before core
initialisation both return 0, which the build's own gates assert.

The minified WebAssembly export aliases (`Ph`/`Oh` for legacy, `Xh`/`Wh` for regular in this
build) are **build-local** and must never be depended on. Always go through the public `Module`
property names.
