Releases: germaneguise/oss-chromatic-console-fpga
Release list
v18.8-uvc-dualres.5: optimization release
v18.8-uvc-dualres.5 — optimization release
Six-commit resource optimization stack over v18.8-uvc-dualres.4. No player-facing behaviour change apart from the one documented
exception below. Every commit in the series was built and measured
individually; the resource figures in each commit message are the
measured values for that exact tree.
Resources
Historical columns as published with v18.8-uvc-dualres.4; every build
reproduced from its tag on the same toolchain, so the columns are
directly comparable.
| v18.8 | uvc-dualres.1 | uvc-dualres.2 | uvc-dualres.3 | uvc-dualres.4 | uvc-dualres.5 | |
|---|---|---|---|---|---|---|
| Logic (LUT+ALU+ROM16) | 20,314 (89%) | 15,316 (67%) | 15,353 (67%) | 15,412 (67%) | 12,654 (55%) | 12,397 (53.8%) |
| - LUT | 15,779 | 13,307 | 13,337 | ~13,370 | 10,731 | 10,701 |
| Registers | 9,862 (42%) | 8,182 (35%) | 8,208 (35%) | ~8,200 (35%) | 6,499 (28%) | 6,427 (27.2%) |
| CLS (the fabric's real ceiling) | 11,036 (96%) | 9,868 (86%) | 9,776 (85%) | 9,891 (86%) | 8,445 (73%) | 8,420 (73.1%) |
| BSRAM | 45 (81%) | 47 (84%) | 47 (84%) | 47 (84%) | 46 (83%) | 42 (75.0%) |
| DSP | 28 (100%) | 17 (61%) | 17 (61%) | 17 (61%) | 16.5 (59%) | 7.5 (26.8%) |
| worst setup slack (ns) | 0.305 | 2.944 | 2.801 | 2.807 | 2.460 | 3.027 |
| worst hold slack (ns) | 0.131 | 0.173 | 0.184 | 0.116 | 0.126 | 0.134 |
BSRAM drops below the official v18.8 release for the first time in the
uvc-dualres line - the features were paid for in blocks at .1 and are
now paid back with three to spare - and DSP reaches 26.8% with the last
encrypted video-path core out of the build. Timing met on every clock.
Per-clock Fmax (MHz, actual vs constraint)
| clock | constraint | v18.8 | uvc-dualres.1 | uvc-dualres.2 | uvc-dualres.3 | uvc-dualres.4 | uvc-dualres.5 |
|---|---|---|---|---|---|---|---|
| sclk | 40.000 | 124.216 | 133.688 | 128.341 | 155.293 | 227.206 | 225.320 |
| usbintsclk | 125.000 | 156.528 | 197.794 | 192.327 | 192.566 | 180.521 | 201.106 |
| pclk | 33.554 | 34.255 | 37.933 | 44.974 | 50.566 | 42.600 | 49.985 |
| hclk | 16.777 | 17.779 | 23.587 | 23.136 | 24.932 | 21.015 | 23.841 |
| gclk | 8.389 | 91.102 | 136.155 | 122.478 | 104.433 | 134.802 | 150.353 |
| xclk | 67.109 | 98.595 | 119.982 | 130.002 | 126.830 | 125.129 | 132.152 |
| PHY_CLKOUT | 60.000 | 67.285 | 84.573 | 78.650 | 89.612 | 93.035 | 83.853 |
| ESP_BOOT_DELAY_COUNTER[0] | 100.000 | 290.296 | 197.071 | 190.462 | 174.978 | - | - |
| db_prescale[0] | 100.000 | - | - | - | - | 204.318 | 171.005 |
Fmax varies substantially between otherwise-unrelated builds (the sclk
column swings ~20% across this stack's own intermediate builds); the
figures above are single-build measurements, and only the margin to
constraint is load-bearing. Every clock clears its constraint at every
commit in the series. The pclk gain at .5 is attributable: colour
conversion shortened from a 6-stage encrypted pipeline to 3 stages.
The stack
Per commit, approximately (each figure measured when the commit landed;
LUT and register deltas of this size sit inside the build-to-build
noise of this design, so they are directional - the BSRAM, DSP and
channel figures are exact):
| commit | change | LUT | REG | other |
|---|---|---|---|---|
134fe01 |
CGB palette pair packed into one BSRAM per palette (mixed-width SDPB, byte write / word read) | +58 | +16 | -2 BSRAM |
1e7f5dd |
sprite stall removed from the blank-screen PPU (its output is a constant; the engine only stalled the pixel counter on stale OAM) | -28 | -18 | -90 ALU |
878c7aa |
UVC feed moved off panel scanout to the emulator's own clock domain; panel line buffer halves to one block | +92 | +5 | -1 BSRAM |
1ea3ab5 |
one DC blocker shared by both audio channels (each used its arithmetic 1 cycle in 256) | +5 | -25 | -118 ALU |
e669ef1 |
one line reader serves the frame-blend and OSD planes, retargeted per frame | -4 | -28 | -1 BSRAM, one MPMC channel freed |
ad599c8 |
encrypted colour space converter replaced by an open implementation, bit-identical | -26 | -16 | -9 DSP, encrypted file removed |
Verification
- The replacement colour space converter is bit-identical to the
encrypted core it replaces: verified exhaustively in simulation over
all 2^18 producible inputs (sim/fifo_video/tb_csc.v), and on-device
against the live encrypted core over the full 2^24 input space with
zero mismatches. The original computes the BT.601 studio-range matrix
with plain floor and no rounding bias; the replacement matches it. - Every intermediate commit builds cleanly with the figures its message
states; the series is bisectable with resource provenance. - All changes exercised on hardware: CGB palettes, LCD-off transitions,
UVC at both advertised resolutions, OSD/menu with frame blending,
audio.
Deliberate behaviour exception
- Frame blending is disabled on frames where the OSD is shown. The
menu covers the screen, so the blend result was never visible; the
single line reader fetches the OSD plane on those frames instead.
Flashing
Flash the whole image so the provenance record and bitstream stay in
lockstep (see the repository README):
openFPGALoader -c gwu2x -f --external-flash --file-type bin chromatic-v18.8-uvc-dualres.5.flash
fpgaflash verify on the device console then measures the bitstream in
flash against the record. The bitstream USERCODE is the short hash of
the tagged commit.
Compatibility
Pairs with MCU release v4.2-uvc-dualres.2; nothing in this stack
changes the FPGA-MCU protocol.
v18.8-uvc-dualres.4
Faster, better, cheaper - optimization release
An unofficial FPGA build for the ModRetro Chromatic. It carries everything
the uvc-dualres line already does - the colour-lossless 320x288 USB capture from
uvc-dualres.1,
the two-rate CDC UART, ESP32 flash bridge and build provenance from
uvc-dualres.2,
and the USB keep-awake fix from
uvc-dualres.3 -
and adds nothing you can see: gameplay, video capture and normal USB traffic
are intended to be byte-identical to uvc-dualres.3. The deliberate exceptions are four
edge cases, each documented in its commit:
- The CDC UART always runs 8N1 now: a host requesting other framing silently
gets 8N1, and GET_LINE_CODING reports it honestly. Baud switching for
esptool is untouched. This UART is the ESP32 development console - if you
never attach a serial terminal or esptool, none of this is visible. - The halved CDC buffer overruns ~2 KB sooner under extreme backpressure
(host attached but not reading while the ESP32 streams). - A disabled audio channel's fade-out can start up to ~24 us later.
- Button edges reach the MCU ~2 us earlier.
What this release changes is the design itself: almost a fifth smaller, so
there is more room to build things.
This is not a ModRetro release. Third-party build; the version register
reports 18.8 with the debug bit set, as before. See Recovery below.
What you get
| v18.8 | uvc-dualres.1 | uvc-dualres.2 | uvc-dualres.3 | uvc-dualres.4 | |
|---|---|---|---|---|---|
| Logic (LUT+ALU+ROM16) | 20,314 (89%) | 15,316 (67%) | 15,353 (67%) | 15,412 (67%) | 12,654 (55%) |
| - LUT | 15,779 | 13,307 | 13,337 | ~13,370 | 10,731 |
| Registers | 9,862 (42%) | 8,182 (35%) | 8,208 (35%) | ~8,200 (35%) | 6,499 (28%) |
| CLS (the fabric's real ceiling) | 11,036 (96%) | 9,868 (86%) | 9,776 (85%) | 9,891 (86%) | 8,445 (73%) |
| BSRAM | 45 (81%) | 47 (84%) | 47 (84%) | 47 (84%) | 46 (83%) |
| DSP | 28 (100%) | 17 (61%) | 17 (61%) | 17 (61%) | 16.5 (59%) |
| worst setup slack (ns) | 0.305 | 2.944 | 2.801 | 2.807 | 2.460 |
| worst hold slack (ns) | 0.131 | 0.173 | 0.184 | 0.116 | 0.126 |
Every build in that table was reproduced from its tag on the same toolchain,
so the columns are directly comparable - and the lineage tells the story:
v18.8 met timing by 0.3 ns with the fabric at 96%; this release meets it by
2.5 ns at 73%.
Where it went, by theme:
- The CGB palettes moved from ~1,000 flip-flops + a wall of muxes into
nearly-empty block RAMs - the single biggest win, over half of the PPU. - A savestate bus that could never be read still had every register wired
to a readback mux; the readback is now constant-folded away (−356 LUT). - The USB audio packet buffer stopped being two 192-bit shift registers.
- The CDC UART hardwires its framing to 8N1 (all the ESP32 ever speaks -
baud switching for esptool is untouched) and lets constant config fold. - Eight button debouncers share one prescaler instead of carrying private
15-bit counters; assorted counters and pointers are cut to the width their
values actually need. - The QSPI write FIFO IP (flags unread, resets tied off) became a plain
dual-clock BSRAM. Its gray-code machinery served nothing and, being
encrypted, could never be pruned.
Per commit, approximately (each figure measured when the commit landed, so
cross-commit folding means the whole slightly outperforms the sum):
| commit | change | LUT | REG | other |
|---|---|---|---|---|
bc00340 |
EP3 CDC FIFOs 4K to 2K | ~0 | ~0 | -3 BSRAM |
07e5143 |
savestate readback constant-folded | -356 | +7 | |
f7a6649 |
CDC UART framing hardwired 8N1 | -81 | -86 | |
144b299 |
shared-tick button debouncers | -69 | -88 | +12 ALU |
07c418d |
UART config fold + 10-bit divider | -97 | -79 | |
b5629b1 |
UAC packet buffer indexed | -204 | -12 | |
6936ced |
CGB palettes into BSRAM | -1,095 | -962 | +4 BSRAM |
d791d27 |
restamp: select + single line buffer | -28 | -21 | -1 DSP, -1 BSRAM |
6a901a6 |
ROM extraction steered (4 cones) | -170 | -107 | -1 BSRAM |
1f598ad |
button syncs 15 to 3 + audio filter drop | -23 | -160 | |
29ba0ad |
i2c divider counter 16 to 5 bits | ~0 | -11 | |
1e0df62 |
width trims (counters, pointers, SRs) | -90 | -95 | -32 ALU |
6df021e |
QSPI write FIFO IP to plain BSRAM | -64 | -60 | |
26bd220 |
audio clock accumulator by gcd, 32 to 12 bits | -35 | -19 | |
4b3c266 |
bit-exact width and dead-logic batch | -116 | -35 | -26 ALU |
Two further changes were built and measured but deliberately left out:
turning off PnR register replication (valid only while the floorplan is this
light) and a narrower audio DC-blocker datapath (filter-equivalent but not
bit-exact). They sit at the top of the branch, excluded by tag placement.
Full per-module and per-clock data, including v18.8 and every uvc-dualres release
rebuilt from its tag for comparison, is in the Resource data appendix below.
Why bother with uvc-dualres.4, if nothing player facing changes?
Headroom. At v18.8 the fabric was 97% full and the DSPs 100% allocated; a
feature the size of a mid-size module was impossible. After this release
roughly 12,000 LUTs, 3,000 slices, 10 block RAMs and 11 DSPs are
available for new features.
Try it
The easy path is CCFL, the browser
loader: connect the Chromatic, pick chromatic-v18.8-uvc-dualres.4.fs, and
Load into SRAM. Nothing is written permanently; a power cycle restores
whatever is in flash. If you like it, flash the .flash image from the same page
(Persistent, Flash FPGA).
For openFPGALoader users, the same two steps from the command line
SRAM test load:
openFPGALoader --cable gwu2x chromatic-v18.8-uvc-dualres.4.fs
Persistent flash:
openFPGALoader -c gwu2x -f --external-flash --file-type bin chromatic-v18.8-uvc-dualres.4.flash
Either way, always flash the whole .flash image, never a bare bitstream
over it: the image carries the provenance blob that fpgaflash verify
checks.
Files
| file | use |
|---|---|
chromatic-v18.8-uvc-dualres.4.flash |
1 MiB config-flash image: bitstream + provenance blob. Flash this. |
chromatic-v18.8-uvc-dualres.4.fs |
bitstream only - SRAM / test loads |
chromatic-v18.8-uvc-dualres.4.flash.manifest |
plaintext blob fields + image sha256 |
SHA256SUMS |
checksums |
bitstream_len = 921955 (0xE1163)
bitstream_crc32 = 0x1bedc27c (over exactly the bitstream; what fpgaflash verify measures)
commit = 4b3c266b3ca0ec585bbba9bfc308f6d9791a73da
USERCODE = 0x4b3c266b
Verify on the device
Unchanged from uvc-dualres.2: fpgaflash verify must report match: true with this
release's CRC and commit; a flash-booted unit reports the commit as its
USERCODE over JTAG. An SRAM load does not set the flash blob or USERCODE.
Identifying what a unit runs
The uvc-dualres.2 identification table, extended:
| reports | debug | image | length | fpgaflash crc <len> |
USERCODE |
|---|---|---|---|---|---|
| 18.8 | 0 | official ModRetro v18.8 | 0xE1170 (921,968) | 0xb68a5f70 |
0x1337d00d |
| 18.8 | 1 | v18.8-uvc-dualres.1 | 0xE117A (921,978) | 0xa0a99866 |
- |
| 18.8 | 1 | v18.8-uvc-dualres.2 | 0xE2B3E (928,574) | 0x2f5a7b02 |
0x29fa6476 |
| 18.8 | 1 | v18.8-uvc-dualres.3 | 0xE1178 (921,976) | 0x1b3d0944 |
0x29d66069 |
| 18.8 | 1 | v18.8-uvc-dualres.4 (this release) | 0xE1163 (921,955) | 0x1bedc27c |
0x4b3c266b |
Verification, honestly stated
Every commit carries its own measured figures, and each was built and compared
in isolation before joining the stack. On hardware, the full stack has been
soaked over: CGB and DMG game colours (the palette move's risk), USB audio,
UVC capture at both resolutions, the menu/OSD path (whose QSPI FIFO was
replaced), buttons and the menu-hold combo, USB CDC including esptool at
921600, and codec initialisation. The two commits that change behaviour on
purpose were excluded (see above).
What a bit-exact claim cannot cover is what nobody thought to test - side-by-
side play against uvc-dualres.3 on anything you care about is exactly the report that
helps.
Recovery
The ModRetro Update Tool restores official firmware from anything here.
Support questions about this build do not belong with ModRetro.
Build it yourself
git clone https://github.com/germaneguise/oss-chromatic-console-fpga && cd oss-chromatic-console-fpga
git checkout v18.8-uvc-dualres.4 && git submodule update --init --recursive
cd esp32t && gw_sh build_dualres.tcl
python tools/pack.py pack --bitstream impl/pnr/evt1_x2_dualres.fs --out chromatic-v18.8-uvc-dualres.4.flash --tag v18.8-uvc-dualres.4USERCODE is stamped from the commit automatically; a dirty tree warns and
marks the stamp unreliable.
Technical detail
The fifteen commits, each with measured figures in its message, are on
dualres-optimizations ; the full diff against uvc-dualres.3 shows everything
that changed. The deliberately deferred pair (replication policy, DC-blocker
width) are the two commits above the release tag.
Appendix: resource data
All builds reproduced from their tags with the era-correct flow on Gowin
V1.9.12.03; uvc-dualres.1 reproduced its historical release figures
digit-for-digit, which validates the method.
Top-level (placed) and timing
| v18.8 | uvc-dualres.1 | uvc-dualres.2 | uvc-dualres.3 | uvc-dualres.4 RC | |
|---|---|---|---|---|---|
| Logic (LUT+ALU+ROM16) | 20,31... |
v18.8-uvc-dualres.3
Hotfix over v18.8-uvc-dualres.2.
With a USB host attached, normal gameplay button presses could be interpreted by the MCU as menu input and corrupt OSD state (the USB keepalive rode the Buttons channel). The keepalive now uses the reserved channel. No MCU firmware update required.
Also: dualres release bitstreams now carry the source commit hash in USERCODE (was the Gowin default checksum).
bitstream_crc32 = 0x1b3d0944(fpgaflash verify window)- USERCODE
29d66069 - Verified on hardware via SRAM load before release.
cart-reader-1.1
Hotfix over cart-reader-1.0.
With a USB host attached, normal gameplay button presses could be interpreted by the MCU as menu input and corrupt OSD state (the USB keepalive rode the Buttons channel). The keepalive now uses the reserved channel — same fix as v18.8-uvc-dualres.3. No MCU firmware update required.
Also: release bitstreams now carry the source commit hash in USERCODE, and the build uses route effort 2, needed at this branch's density.
bitstream_crc32 = 0x6b3a29af(fpgaflash verify window)- USERCODE
a0dc68e6 - Verified on hardware (SRAM load + fgbweb WebUSB/JTAG cart-dump test) before release.
cart-reader-1.0
A cartridge reader built into the console's normal gateware, so the Chromatic
still boots and plays games. Open the USB serial port at the magic rate and it
hands over the cart bus; disconnect and it hands it back. No reflashing, no
button combo, no power cycle.
Based on fredemmott's cart reader, riding on top of the UVC/dual-resolution
release. This is the gateware fgbweb
loads for you automatically.
Which file do I want?
The .fs. It loads into the FPGA's SRAM and is gone the moment you power
off, so there's nothing to undo. That's what fgbweb uses.
Take the .flash only if you specifically want this to be your console's
flashed firmware. It's a full 1 MB config flash image: it survives power-off
and can brick the console until you reflash it. Write the whole .flash image, never
just the bitstream — a partial write leaves stale metadata behind.
Verify it
sha256sum -c SHA256SUMS
python esp32t/tools/pack.py verify --image chromatic-cart-reader-1.0.flash
On the device, fpgaflash verify should report bitstream_crc32 = 0x149ba7ce.
Tested on hardware
- Boots and plays a game with the reader built in
- Dumps Zelda: Oracle of Ages at CRC32 3800A387 — matches the
known-good image - Works through both openFPGALoader and fgbweb's WebUSB loader
- Takes the cart bus and gives it back cleanly, over repeated sessions
Built with gw_sh 1.9.12.03. All clocks meet timing. It's a tight fit — 95% of
CLS, 639 to spare, so there isn't much room left for anything else. That makes loading this on demand over SRAM when needed a good way to go.
Known issue
cart_audio powers up asserted and stays that way until the first AUDIO write.
Upstream that's harmless because the game core isn't there; here it is, so the
pin sits low while a game runs. Almost no cartridges use audio-in and we haven't
seen a problem, but check this first if a game acts up.
AUDIO-as-write-enable is wired but untested. The ModRetro cartridge uses WR.
Where it came from
Commit 78fdc9ae, on top of v18.8-uvc-dualres.2 (29fa647). The bitstream
writes 0x78fdc9ae into USERCODE, so a running console can tell you which
commit it's from.
Heads up: openFPGALoader reads USERCODE back wrong on this chip. On GW5A,
JTAG instruction 0x0a is WRITE_USERCODE, not the checksum register, so its
checksum step overwrites the value. You'll see the bitstream's checksum instead
of the commit. That's a bug in the tool, not in this file.
GPL-3.0, same as the base.
v18.8-uvc-dualres.2
This exact build has been flashed and verified on hardware: fpgaflash verify match 0x2f5a7b02, blob commit 29fa647, USERCODE 0x29FA6476 read back, console alive after idle.
Fork pre-release of the UVC / pixel-perfect capture stack, built from uvc-dual-resolution-with-resource-savings @ 29fa647 (= PR ModRetro#10 branch + the version debug bit). Not a ModRetro build: the version register reports 18.8 with the debug bit set.
What's new since v18.8-uvc-dualres.1
- Two-rate CDC UART (115200 / 921600): any other requested rate falls back to 115200, so existing tools keep working.
- ESP32 ↔ config-flash bridge over the display QSPI pins with its own chip select (I2S_WS / IO25): the MCU can read the FPGA's config flash directly, plain or quad-output (
0x6B). ~16 flops. - Keep-awake while a USB host is attached: the FPGA sends a Buttons message every 4 ms (HS) / 32 ms (FS) while SOF is present, so the MCU's 100 ms light-sleep timer is petted and the debug console no longer goes deaf a few seconds after boot. Sleeps normally once the host is gone; chargers don't count.
- Build provenance: USERCODE = source commit (
0x29fa6476), and the 1 MiB flash image carries a plaintextCHRMblob in sector0x0FF000with commit, origin, build time, tag, the bitstream's length / CRC-32 / sha256, and a source URL. - Host-safe console open: DTR/RTS from the CDC port drive the ESP32's EN/IO0, and some hosts (Chrome's Web Serial) assert them on every open, resetting or strapping the ESP32. Opening with a baud whose bit 0 is set (115201 or 921601) arms a one-shot that ignores the next DTR/RTS change; bit 0 is dropped from the rate (the link runs at 115200/921600 and
GET_LINE_CODINGreports that). A later DTR/RTS change behaves normally, so esptool-style resets still work when you want them. - IDE project (
evt1_x2.gprj) file list re-synced withbuild.tcl; it opens and synthesises again.
MCU side: v4.2-uvc-dualres.2 (branch fpga-flash-bridge-bringup): fpgaflash id|crc [len]|blob|verify|read|sfdp|status|quad|speed and buildinfo.
Files
| file | use |
|---|---|
chromatic-v18.8-uvc-dualres.2.flash |
1 MiB raw config-flash image — bitstream + padding + provenance blob. Flash the whole image. (.flash, not .bin: .bin is the MCU firmware.) |
chromatic-v18.8-uvc-dualres.2.fs |
bitstream only — SRAM / test loads only (openFPGALoader -c gwu2x file.fs). An SRAM load does not update the provenance blob or the USERCODE; for an install, flash the .flash image. |
chromatic-v18.8-uvc-dualres.2.flash.manifest |
plaintext copy of the blob fields + image sha256 |
SHA256SUMS |
checksums of the above |
bitstream_len = 928574 (0xE2B3E)
bitstream_crc32 = 0x2f5a7b02 (CRC-32, zlib polynomial, over exactly the bitstream)
commit = 29fa6476c12858146934c970bab8304e7e549eb5
Flash
openFPGALoader -c gwu2x -f --external-flash --file-type bin chromatic-v18.8-uvc-dualres.2.flash
(--file-type bin because the loader guesses the format from the extension.) JTAG flash writes erase the SRAM configuration; power-cycle afterwards to boot the new image. Always flash the whole image: a bare .fs written by another tool leaves whatever was in the blob sector behind (a stale blob), which fpgaflash verify then reports as a mismatch.
MCU firmware (from the MCU repo) flashes app-only at 0x10000, 115200 baud. On the ESP32-U4WDH the esptool stub needs the eFuse SPI pad map or it cannot see the flash: esptool --chip esp32 --port COMx --baud 115200 write-flash --spi-connection 6,17,8,11,16 0x10000 mcu_fw.bin (or --no-stub).
Verify on the device
With the bridge firmware on the MCU, over the USB console:
fpgaflash verify -> { "match": true, "bitstream_crc32": "0x2f5a7b02", "expected": "0x2f5a7b02",
"length": 928574, "commit": "29fa6476...", "release": "v18.8-uvc-dualres.2", "origin": "build" }
fpgaflash blob -> the CHRM record as packed
buildinfo -> MCU source commit, build time, IDF version, ELF/image sha256, image verified, FPGA version + debug bit
match: true means the bitstream in flash is exactly the one the blob names. match: false means the blob is stale or foreign (a bare bitstream was written over it) — do not trust its commit; identify the unit from the table below instead. fwversion is unchanged so existing tools keep parsing it. esp32t/tools/pack.py verify --image <dump> checks a flash dump against the same fields offline.
Over JTAG, a flash-booted unit also reports the source commit as its USERCODE:
programmer_cli --cable-index 0 --device GW5AR-25A --run 0 -> User Code is: 0x29FA6476
(An SRAM load does not set this register, so only a flash boot reports the stamp.)
Identifying what a unit runs (no blob needed)
A unit flashed with a bare .fs (MRUpdater, Gowin programmer) carries no usable blob, but can still be identified: the FPGA's reported version picks a candidate, and a CRC over exactly that build's published length settles it — fpgaflash crc <hexlen> — independent of whatever older bytes sit after it in flash.
| reports | debug | image | length | fpgaflash crc <len> |
USERCODE |
|---|---|---|---|---|---|
| 18.8 | 0 | official ModRetro v18.8 (v18.8_20251224.fs) |
0xE1170 (921,968) |
0xb68a5f70 |
0x1337d00d |
| 18.8 | 1 | germaneguise v18.8-uvc-dualres.1 (evt1_x2_dualres.fs) |
0xE117A (921,978) |
0xa0a99866 |
— |
| 18.8 | 1 | this release (chromatic-v18.8-uvc-dualres.2.flash) |
0xE2B3E (928,574) |
0x2f5a7b02 |
0x29fa6476 |
Decision tree: blob verifies → ours; else table by reported version → CRC over the published length → official / known unofficial; else "unofficial build reporting 18.8". (The updater automates this; over the MCU path a bitstream with the bridge must be running — on a stock bitstream every read returns zeros, JEDEC 000000.)
Build it yourself
Both repos document the exact flow in their READMEs; in short:
# FPGA (Gowin V1.9.12.03): bitstream with USERCODE = commit, then the 1 MiB image + manifest
git clone https://github.com/germaneguise/oss-chromatic-console-fpga && cd oss-chromatic-console-fpga
git checkout v18.8-uvc-dualres.2 && git submodule update --init --recursive
cd esp32t && gw_sh build_evt1_x2.tcl
python tools/pack.py pack --bitstream impl/pnr/evt1_x2_v07.bin --out chromatic-v18.8-uvc-dualres.2.flash --tag v18.8-uvc-dualres.2
python tools/pack.py verify --image chromatic-v18.8-uvc-dualres.2.flash
# MCU (ESP-IDF v5.3): app image whose `buildinfo` names the commit
git clone https://github.com/germaneguise/oss-chromatic-console-mcu && cd oss-chromatic-console-mcu
git checkout fpga-flash-bridge-bringup
idf.py build # build/mcu_fw.bin
esptool --chip esp32 --port COMx --baud 115200 write-flash --spi-connection 6,17,8,11,16 0x10000 build/mcu_fw.binBuilds are deterministic: two builds of identical sources (different commits, docs-only change) produced bitstreams that differ in exactly the 4 USERCODE bytes, so a rebuild of this tag reproduces this bitstream byte-for-byte apart from that stamp.
Resources / timing (Gowin 1.9.12.03, build_evt1_x2.tcl)
| resource | PR ModRetro#10 head (7308b5e) |
this release | delta |
|---|---|---|---|
| Logic (LUT+ALU+ROM16) | 15419 | 15342 | −77 |
| — LUT | 13412 | 13337 | −75 |
| — ALU | 1898 | 1896 | −2 |
| Registers | 8007 | 8027 | +20 |
| CLS (of 11520) | 9887 | 9844 | −43 |
| BSRAM | 47 | 47 | 0 |
| DSP | 17 | 17 | 0 |
The +20 registers are the real cost: 16 for the quad-read phase FSM of the bridge, 3 for the keep-awake synchroniser/edge detect, 1 for the CDC one-shot (its second flop merged). LUT deltas of this size are build-to-build placement noise (untouched modules move by ±1% between runs).
| clock | constraint (MHz) | Fmax, PR ModRetro#10 head | Fmax, this release |
|---|---|---|---|
| sclk | 40.0 | 115.0 | 136.9 |
| usbintsclk | 125.0 | 183.0 | 145.4 |
| pclk | 33.55 | 40.2 | 35.3 |
| hclk | 16.78 | 19.5 | 20.3 |
| gclk | 8.39 | 104.2 | 79.7 |
| xclk | 67.11 | 109.7 | 107.8 |
| PHY_CLKOUT | 60.0 | 65.2 | 70.1 |
Total negative slack 0.000 on every clock, setup and hold. Fmax varies with placement from build to build; what matters is that every clock clears its constraint. pclk's margin is the pre-existing half-cycle cartridge path family, present in every build.
Sharper colour over USB capture - 320x288 UVC for the Chromatic
Sharper colour over USB capture — 320x288 UVC for the Chromatic
An unofficial FPGA build for the ModRetro Chromatic that adds a 320x288 USB
capture mode alongside the stock 160x144, to work around a colour loss in the
video format the Chromatic streams.
This is not a ModRetro release. It is a third-party build, and it identifies
itself as one — see Recovery below before flashing anything.
What you get
The Chromatic streams USB video as YUY2, which stores one colour sample per
pair of horizontal pixels. At the native 160x144 those two pixels are genuinely
different, so their colour gets averaged together and horizontal colour
resolution is halved. On a Game Boy that hurts, because the content is full of
hard-edged tile and sprite boundaries with unrelated colours either side.
This build also offers 320x288, which is the same picture with every pixel
doubled. It adds no detail — but because each pixel pair is now the same pixel
twice, averaging them changes nothing and the colour comes through intact.
320x288 is colour-lossless. 160x144 is not. Both are offered; 320x288 is the
default. Same 60 fps either way.
There are also a number of resource and timing optimisations bundled in, which is
what freed the space on the FPGA to fit the above.
Try it without committing to anything
The safe way to test is to load it into the FPGA's SRAM. Nothing is written
permanently, and a power cycle puts everything back exactly as it was.
openFPGALoader --cable gwu2x evt1_x2_dualres.fs
The Chromatic must be powered on. If it works and you like it, flash it properly:
openFPGALoader --write-flash --cable gwu2x --reset evt1_x2_dualres.fs
openFPGALoader must be built with GWU2X cable support. The Gowin Programmer
works too, and needs the GWU2X driver installed.
Try SRAM first. There is no reason to write flash until you have seen it
work on your own unit.
Recovery
The ModRetro Update Tool restores official firmware. That is the way back
from anything here, including a flash write you regret.
This build sets the FPGA version's debug bit, so a unit running it reports as a
modified build rather than as stock v18.8. That is deliberate — it means you, and
anyone helping you, can always tell what is actually on the hardware.
Please do not take support questions about this build to ModRetro. It is not
theirs.
Known caveats
It uses more USB bandwidth by default. 320x288 reserves roughly a third of a
USB 2.0 controller's isochronous budget — about double what stock v18.8 used.
Two of these streaming on one controller is near the limit and a third will not
start at all. If you run other webcams, or two Chromatics, this may matter.
Selecting 160x144 drops the reservation back to exactly what stock used. Symptoms
are the stream failing to open rather than glitching; a port on a different
controller usually fixes it.
Only tested on Windows 11. Measured against the Windows USB stack and
DirectShow. Linux (uvcvideo) and macOS are untested, and host stacks differ most
in exactly the areas this touches — alternate-setting selection and
high-bandwidth isochronous allocation. Reports from either are the main thing
I'm looking for.
Built with Gowin 1.9.12.03, which the project README says is unsupported (it
pins 1.9.9.03). It builds and runs, but has not been checked on the pinned
version.
Verification is real but not exhaustive. Both resolutions hold 60 fps with
every frame complete on the wire, over repeated open/close cycles. Some
individual changes are marked in their commits as tested only briefly, and those
say so.
If you're testing, this is what's useful to know
- Which OS and capture application
- Whether 320x288 opens, and whether it holds 60 fps
- Whether 160x144 still works
- Anything else on the same USB controller
- Colour compared to the stock build, if you can compare side by side
Technical detail
The eleven commits, each carrying its own measured resource and timing figures,
are on the
uvc-dual-resolution-with-resource-savings
branch, and the
full diff against v18.8
shows everything that changed.
This bitstream was built from commit
ade4a16
with esp32t/build_dualres.tcl, the repo's own build script — clone the tag and
run it and you should get the same design.
Short version of where the space came from: the FPGA was effectively full before
this, with DSP at 28/28 and CLS at 97%. Six of the commits reclaim room (the
largest single win being two synthesis attributes on the GBC palette arrays), and
the video work then spends part of it. The design ends up smaller than stock on
every resource but two block RAMs.