Skip to content

Releases: germaneguise/oss-chromatic-console-fpga

v18.8-uvc-dualres.5: optimization release

Choose a tag to compare

@germaneguise germaneguise released this 28 Aug 06:34

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

  1. 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

Choose a tag to compare

@germaneguise germaneguise released this 24 Aug 18:41

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.4

USERCODE 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...
Read more

v18.8-uvc-dualres.3

Choose a tag to compare

@germaneguise germaneguise released this 24 Aug 03:49

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

Choose a tag to compare

@germaneguise germaneguise released this 24 Aug 04:34

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

Choose a tag to compare

@germaneguise germaneguise released this 23 Aug 03:20

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

v18.8-uvc-dualres.2 Pre-release
Pre-release

Choose a tag to compare

@germaneguise germaneguise released this 22 Aug 19:28

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 plaintext CHRM blob in sector 0x0FF000 with 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_CODING reports 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 with build.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.bin

Builds 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

Choose a tag to compare

@germaneguise germaneguise released this 14 Aug 20:29

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.