#!/usr/bin/env python3 """The DECODER-FREE PACKED player, priced against the measured cost model. python3 tools/analysis/29_packed_player.py [container.dlx] [--kbps R] THE QUESTION, and why it is being asked again. FINDINGS 44.7 removed the codec and asked what a player that just puts literal frames on screen would cost. It answered "it fits the clocks and dies on the medium": 1,152 KB/s and 1.61 GB, because 256-colour GVRAM's default write path throws away the high byte of every word and a picture byte therefore costs two disc bytes. 46.5/47.1 then found the off switch -- CRTC R20 bit 11 -- and 47.2 built the layout and rendered it pixel-exactly on both emulators at 1.0 B/pixel. 47.5 re-derived the budget on that and withdrew 44.7's conclusion CONDITIONALLY. Everything in 47.5 is arithmetic over a cost model that has since been REPLACED. When it was written the transport was an unmeasured `c`; sessions 25b-28 put the transport on the 68000 and measured it (58.2: PIO is 87.28 clk/B), put it on the DMAC and bounded it (59.2: this machine can run dual-address only, and a dual-address byte has a 9 clk/B FLOOR), and re-derived what a frame can afford (59.7/60.7: 6.69 clk/B on the gate container). 47.6.1 also filed the CPU paint cost as an ASSUMPTION -- "the `movem` shape of the packed writes is an assumption", no clock in 47.5 measured. So this tool re-asks 44.7's question with: * the paint MEASURED, not assumed -- tools/bench/blit.s V8 is V1 with 128 words a row instead of 256, and tools/bench/blit.lua times it next to V1, V2 and V3 in the same run, so the packed number is quoted against a variant whose value (53.6%) is a session-9 result that has not moved; * the transport swept over the SAME `W` ladder 15_bus_occupancy.py uses, every rung of it sourced or measured (buscost.py); * the audio DMA charged, at the rate the IPL ROM's own channel-3 setup implies (21_iplrom_dmac.py) -- 60.x's rule that a budget debits I/O; * and the wire and the volume stated for each, because 44.7's answer was never about clocks. WHAT IT DOES NOT DO. It does not settle 47.4 -- whether buffer mode BLANKS the graphics layer, which MAME asserts and px68k is silent about (48.1), and which needs a real board. It PRICES both branches instead, and the blanking section is where the measured paint earns its keep: the black interval is the paint, and until now the paint was a range read off an unpacked measurement ("~27% to ~54%", 48.3) rather than a number. """ import sys, os, re, argparse, csv sys.path.insert(0, "tools/encoder") sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import numpy as np from dlx import DLX import buscost as B CPUHZ = 10e6 # stock X68000, MAME 0.277 x68k.cpp:1133 GAME_S = 22.8 * 60 # the full-disc survey's runtime (ROADMAP C1) ap = argparse.ArgumentParser() ap.add_argument("container", nargs="?", default="tmp/rc_fr_singe_scsi_span.dlx", help="the CODEC baseline this is compared against") ap.add_argument("--csv", default="tmp/c68k_frames.csv", help="per-frame C68K measurement of that container") ap.add_argument("--blit-log", default="tmp/blit_v8.log", help="tools/bench/blit.lua's log -- where the MEASURED paint " "costs are read from. No defaults are compiled in.") ap.add_argument("--kbps", type=float, default=None, help="a delivery rate to score the wire against. OPTIONAL and " "there is no default (FINDINGS 50).") a = ap.parse_args() # --- the measured paint, read out of the run's own log --------------------- # NOT transcribed into this file. A constant copied out of a log is a constant # that stops tracking the log, and this project has been caught by a stale # number twice (60.8). If the log is not there the tool refuses rather than # substituting a plausible one. if not os.path.exists(a.blit_log): sys.exit(f"missing {a.blit_log} -- run tools/bench/blit.lua first:\n" f" cd tmp && mame x68000 -bios ipl10 -ramsize 2M -video soft " f"-window -sound none -nothrottle -plugins \\\n" f" -autoboot_script ../tools/bench/blit.lua -seconds_to_run 60") blit = {} for line in open(a.blit_log, errors="replace"): m = re.search(r"V(\d+)\s+(\d+) cyc", line) if m: blit[int(m.group(1))] = int(m.group(2)) for v in (1, 2, 3, 4, 8, 9, 10): if v not in blit: sys.exit(f"{a.blit_log} has no V{v} result -- the summary is incomplete, " f"so the run did not finish and nothing here can be quoted.") # --- the codec baseline: the container, and its MEASURED decode ------------ d = DLX(a.container) FPS = d.fps FRAME_CLK = CPUHZ / FPS meas = {} if os.path.exists(a.csv): for r in csv.DictReader(open(a.csv)): meas[int(r["frame"])] = int(r["cycles"]) if not meas: sys.exit(f"missing {a.csv} -- the codec row's decode term is MEASURED and " f"there is no derived stand-in for it.") NF = max(meas) + 1 codec_decode = np.mean([meas[f] for f in range(NF)]) codec_bpf = sum(d.record_lengths()[:NF]) / NF # the PADDED record (60.7) # --- geometry, which is where the decoder-free rows come from ------------- W_PX, H_PX = d.W, d.H NPX = W_PX * H_PX UNPACKED_BPF = NPX * 2 # one pixel per word, high byte discarded PACKED_BPF = NPX * 1 # R20 bit 11 + page scroll (47.2, measured) aud_bpf = B.ADPCM_BYTES_PER_S / FPS AUD_CLK = aud_bpf * B.ADPCM_CLK_BYTE_BEST # best case, so every row is # the optimistic end # A device->GVRAM channel cannot walk a 1024-byte line stride inside one # transfer: it writes a contiguous run. 192 rows therefore need 192 array-chain # entries -- and SESSION 29 RAN THAT, off the disc, through src/player/dma.i's # DM_BARV/DM_BTCV: eight rows at the 1024 B stride landed from ONE channel start # with the CPU halted throughout (tools/bench/dma_run.sh, `[chain]`). So the # MECHANISM is demonstrated and the CPU does not restart the channel per row. # The COST is still datasheet arithmetic -- 36 clocks an entry, Fig 4-25 sheet 1, # buscost.DMA_CHAIN_CLK -- because MAME's DMAC runs on wall-clock attotimes and # cannot be asked what anything costs (42.5). CHAIN_CLK = H_PX * B.DMA_CHAIN_CLK print(f"""{a.container}: {NF} frames of {W_PX}x{H_PX} at {FPS:g} fps frame slot on a 10 MHz 68000: {FRAME_CLK:,.0f} clocks paint costs MEASURED by tools/bench/blit.lua, read from {a.blit_log}: V1 unpacked movem blit {blit[1]:>9,} clk {100*blit[1]/FRAME_CLK:5.1f}% (96 KB read + 96 KB write) V2 byte-source expansion {blit[2]:>9,} clk {100*blit[2]/FRAME_CLK:5.1f}% (48 KB read + 96 KB write) V3 write-only floor {blit[3]:>9,} clk {100*blit[3]/FRAME_CLK:5.1f}% (no source read at all) V8 PACKED movem blit {blit[8]:>9,} clk {100*blit[8]/FRAME_CLK:5.1f}% (48 KB read + 48 KB write) V8 is {100*blit[8]/blit[1]:.1f}% of V1 and {100*blit[8]/blit[3]:.1f}% of V3 -- so PACKED PAINT COSTS WHAT THE UNPACKED PATH PAYS TO WRITE ALONE, with its source read thrown in free. It is not exactly half of V1 because the 192-row loop does not halve with the words: per word V1 is {blit[1]/(NPX):.3f} clk and V8 is {blit[8]/(NPX//2):.3f}.""") # --- the architectures ---------------------------------------------------- # Each is (label, bytes on the wire per frame, CPU clocks per frame that are # NOT the transport, and whether the transport lands in GVRAM or in RAM). ARCH = [ ("CODEC, CPU-decoded (the shipping design)", codec_bpf, codec_decode, "ring"), ("free / DMAC device->GVRAM / unpacked", UNPACKED_BPF, CHAIN_CLK, "gvram"), ("free / DMAC device->GVRAM / PACKED", PACKED_BPF, CHAIN_CLK, "gvram"), ("free / CPU-painted / unpacked, 2 B/px wire", UNPACKED_BPF, blit[1], "ring"), ("free / CPU-painted / unpacked, 1 B/px wire", PACKED_BPF, blit[2], "ring"), ("free / CPU-painted / PACKED", PACKED_BPF, blit[8], "ring"), ] LADDER = [ (5.0, "single address, held -- needs a request line (B3)"), (9.0, "dual address, held -- the FLOOR (59.2/59.7)"), (12.0, "single address, arbitrated"), (16.0, "what the ROM programs for SASI, best"), (19.0, "what the ROM programs for SASI, worst"), (87.28, "PIO -- MEASURED, 58.2"), ] print("\n" + "=" * 78) print("WHAT EACH ARCHITECTURE COSTS A FRAME, over the transport ladder\n") print(f" audio DMA is charged in every row at {AUD_CLK:,.0f} clk " f"({100*AUD_CLK/FRAME_CLK:.2f}%), best case.\n") hdr = f" {'architecture':<44}{'B/frame':>9}" + "".join(f"{f'W={w:g}':>9}" for w, _ in LADDER) print(hdr) print(" " + "-" * (len(hdr) - 2)) for label, bpf, cpu, dest in ARCH: cells = [] for w, _ in LADDER: tot = bpf * w + cpu + AUD_CLK pct = 100 * tot / FRAME_CLK cells.append(f"{pct:>8.1f}%" if pct < 1000 else f"{pct:>8.0f}%") print(f" {label:<44}{bpf:>9,.0f}" + "".join(cells)) print(f""" 100% is the frame deadline. Every cell is CPU work plus transport plus best-case audio; none of them overlap, because the 68000 has no cache and a two-word prefetch queue that empties at once (buscost.DMA_OVERLAPS = False). THE TWO ROWS THAT MATTER ARE THE FLOOR COLUMN, W=9, because 59.2 found that the only configurations this machine can be shown to run are dual-address, and a dual-address byte is a 4-clock read of the device plus a 5-clock write to memory. Every column left of it is a hardware fact nobody here has.""") # --- the wire, which is what 44.7 actually died on ------------------------ print("\n" + "=" * 78) print("THE WIRE AND THE MEDIUM -- 44.7's real objection\n") print(f" {'architecture':<44}{'B/frame':>9}{'KB/s':>9}{'GB for 22.8 min':>18}") print(" " + "-" * 78) seen = set() for label, bpf, cpu, dest in ARCH: kbs = bpf * FPS / 1024 gb = bpf * FPS * GAME_S / 1e9 print(f" {label:<44}{bpf:>9,.0f}{kbs:>9.1f}{gb:>18.2f}") print(f""" The codec row is the gate container, which is deliberately the heaviest thing the encoder emits (59.7). The default `need` recipe is 267.9 KB/s and E7's byte target at the 9 clk/B floor is 327 KB/s (60.7). SO THE PACKED DECODER-FREE PLAYER ASKS FOR {PACKED_BPF*FPS/1024:.0f} KB/s -- {PACKED_BPF*FPS/1024/327:.2f}x E7's target and {PACKED_BPF*FPS/1024/(codec_bpf*FPS/1024):.2f}x the gate container -- and it asks for it AT A FIXED RATE. A codec's bitrate is a lever; a literal frame's is geometry, and there is no scene in the picture that costs less than another.""") if a.kbps: R = a.kbps * 1024 print(f"\n against a supplied {a.kbps:g} KB/s:") for label, bpf, cpu, dest in ARCH: need = bpf * FPS print(f" {label:<44}{'FITS' if need <= R else 'SHORT BY '}" f"{'' if need <= R else f'{(need-R)/1024:.0f} KB/s'}" f" ({need/1024:.0f} KB/s wanted)") # --- 47.4's two branches, priced ----------------------------------------- print("\n" + "=" * 78) print("IF BUFFER MODE BLANKS THE LAYER (47.4 / 48, MAME's reading)\n") print(""" R20 bit 11 only has to be SET across the GVRAM writes, so the black interval is the paint and not the frame -- and which paint depends on where the transport lands. That asymmetry has not been stated before:\n""") print(f" {'architecture':<44}{'black interval':>16} {'set for':<14}") print(" " + "-" * 78) for label, bpf, cpu, dest in ARCH[1:]: if dest == "gvram": # the channel writes GVRAM, so the bit is set for the whole transfer # DMA rungs only: a PIO transport is not a channel writing GVRAM, so # 87.28 has no meaning in a device->GVRAM row. rows = [bpf * w + CHAIN_CLK for w, _ in LADDER if w < 20] span = f"{100*min(rows)/FRAME_CLK:.0f}%..{100*max(rows)/FRAME_CLK:.0f}%" note = "the whole DMA" else: span = f"{100*cpu/FRAME_CLK:.1f}%" note = "the blit only" print(f" {label:<44}{span:>16} {note:<14}") print(f""" THE CPU-PAINTED PACKED PATH HAS THE SMALLEST BLACK WINDOW OF ANY OF THEM -- {100*blit[8]/FRAME_CLK:.1f}% -- because its transport lands in RAM, where bit 11 is irrelevant, and only the {blit[8]:,}-clock blit needs the bit set. The DMAC-direct path, which is cheaper in clocks at every rung of the ladder, is the one that must hold the bit across its whole transfer. Under MAME's reading the cheap architecture is the dark one. Both are a strobe at the frame rate over the whole picture, and the packed layout has no page to flip to: both 256-colour pages carry picture, which is the entire point of it (48.3). {100*blit[8]/FRAME_CLK:.1f}% black at 12 Hz is not a tear. IF PX68K IS RIGHT AND IT DOES NOT BLANK, every number above stands as written. Neither emulator is authority and 48.1 is why the prior leans MAME's way: MAME asserts the semantic twice and deliberately, px68k's display path never reads the bit at all. That is an assertion against a silence, not a tie, and it is settled by a board and the two-line probe in tools/bench/probe_bit11_blank.lua.""") # --- 47.6.4: does the CODEC survive the packed layout? -------------------- # Open since session 16 and never touched: "under the packed layout a word spans # two columns 128 apart. Whether the existing codec survives that is untouched." # There are exactly two ways it could, and blit.s V9 and V10 are them. sk_blocks = sk_tot = pair_sk = pair_tot = 0 for f in range(NF): m = d.modes(f).reshape(d.nby, d.nbx) L, R = m[:, :d.nbx // 2], m[:, d.nbx // 2:] sk_blocks += int((m == 0).sum()); sk_tot += m.size pair_sk += int(((L == 0) & (R == 0)).sum()); pair_tot += L.size paint_now = 1 - sk_blocks / sk_tot paint_pair = 1 - pair_sk / pair_tot print("\n" + "=" * 78) print("CAN THE CODEC BE PACKED TOO? -- 47.6.4, open since session 16\n") print(f""" A 4x4 block owns four bytes at STRIDE 2 under the packed layout, because the high bytes of its four words belong to the block 128 columns away. So a block decoder has two options and neither is free: {'V4 block order, UNPACKED (the shipping shape)':<52}{blit[4]:>9,} clk {100*blit[4]/FRAME_CLK:5.1f}% {'V9 block order, PACKED, 16 move.b at stride 2':<52}{blit[9]:>9,} clk {100*blit[9]/FRAME_CLK:5.1f}% {'V10 block order, PACKED, blocks PAIRED (movem back)':<52}{blit[10]:>9,} clk {100*blit[10]/FRAME_CLK:5.1f}% V9 IS {100*blit[9]/blit[4]-100:.0f}% DEARER THAN V4, not cheaper. Packing buys a block decoder nothing on the wire either -- a codeword is already one byte a pixel -- so that route buys NOTHING and costs {blit[9]-blit[4]:,} clocks a frame to buy it. V10 halves the paint, and pays for it in the mode map. A pair skips only if BOTH its blocks skip, and on this container: SKIP blocks now {100*sk_blocks/sk_tot:5.1f}% painted now {100*paint_now:5.1f}% SKIP block PAIRS {100*pair_sk/pair_tot:5.1f}% painted paired {100*paint_pair:5.1f}% So pairing paints {paint_pair/paint_now:.2f}x as many blocks for {blit[10]/blit[4]:.2f}x the paint per block -- {100*(paint_pair/paint_now)*(blit[10]/blit[4])-100:+.0f}% on the clock, and about {100*(paint_pair/paint_now-1):+.0f}% on the BYTES, because a coded block is bytes in the container whether its half of the pair changed or not. E7 needs the bytes DOWN {100*(codec_bpf*FPS/1024)/327-100:.0f}%. SO PACKING BELONGS TO THE LITERAL PLAYER AND ONLY TO IT. 47.6.4 is closed: the packed layout is not an upgrade the existing codec can take, it is the thing you get INSTEAD of the codec.""") # --- the palette, which is where the literal player stops being a compromise -- # 46.3 measured these while pricing the TEXT PLANE and the 256-colour rows were # only there for scale. They answer a question nobody put to them: a literal # player has no codebooks, so it is not tied to a scene-wide palette the way the # codec is (vq.scene_palette exists BECAUSE codewords are indices into it), and # per-frame palettes become legal. Re-run 18_text_plane_16col.py to reproduce. PSNR_SHIPPED = 29.19 # docs/STATUS.md, --spans all, c=5, 496.7 KB/s PSNR_SCENE_256 = 31.33 # 18_text_plane_16col.py, tmp/fr_singe, 120 frames PSNR_FRAME_256 = 34.08 # the same window, per-frame palettes PAL_BYTES = 512 # 256 entries x 1 word pal_bpf = PACKED_BPF + PAL_BYTES # The palette write, DERIVED from a MEASURED per-word constant: V8 moves a word # into GVRAM for blit[8]/(NPX//2) clocks and the palette is 256 consecutive # words at $E82000 in the same movem shape. pal_clk = 256 * blit[8] / (NPX // 2) print("\n" + "=" * 78) print("AND THE PICTURE IS BETTER, WHICH NOBODY HAD ASKED\n") print(f""" PSNR against the 24-bit source, 18_text_plane_16col.py over the same 120-frame window the whole tree is measured on: {'shipping container (the codec, as it ships)':<48}{PSNR_SHIPPED:6.2f} dB {'256 colours, SCENE palette -- the codec CEILING':<48}{PSNR_SCENE_256:6.2f} dB {'256 colours, PER-FRAME palette':<48}{PSNR_FRAME_256:6.2f} dB THE MIDDLE ROW IS A CEILING AND NOT A RIVAL. Every codeword the codec emits is an index INTO the scene palette, so no amount of bitrate takes it past {PSNR_SCENE_256:.2f} dB; it spends {codec_bpf*FPS/1024:.0f} KB/s to get within {PSNR_SCENE_256-PSNR_SHIPPED:.2f} dB of it. A LITERAL FRAME HAS NO CODEBOOKS, so the scene palette is not forced on it, and the bottom row is what it simply IS -- {PSNR_FRAME_256-PSNR_SHIPPED:+.2f} dB on the shipping container and {PSNR_FRAME_256-PSNR_SCENE_256:+.2f} dB past the ceiling the codec cannot cross. WHAT THE PER-FRAME PALETTE COSTS: on the wire {PAL_BYTES} B a frame -> {pal_bpf:,} B, {pal_bpf*FPS/1024:.1f} KB/s (+{100*PAL_BYTES/PACKED_BPF:.1f}%) in clocks ~{pal_clk:,.0f} ({100*pal_clk/FRAME_CLK:.2f}% of a frame) if the CPU writes it, DERIVED from V8's measured {blit[8]/(NPX//2):.3f} clk/word in the same movem shape in colours 254, not 256: the packed layout spends index 0 on the transparency key and puts black at 255 (47.2, prep_frame.py --pack-transparent), against --reserve-black's one entry. The tree has already measured a reserved entry at 0.04 dB (60.3), so this is noise against {PSNR_FRAME_256-PSNR_SHIPPED:+.2f}. SETTLED IN SESSION 30, AND THE ANSWER IS YES (FINDINGS 62): a channel writes the palette registers at $E82000 byte-exact, and ONE array-chained start crosses from those registers into GVRAM -- so the palette IS a 193rd chain entry and the clocks row above is what the CPU pays only if it does the write itself. dmagate.s runs 7-9. What that does NOT settle is the board: MAME maps the palette to palette_device over memory_array, whose write16 is a plain COMBINE_DATA, so there is no handler that could refuse a byte write and the model cannot discriminate. ROADMAP B4. AND THE PSNR FIGURES ARE PIL's MEDIANCUT, not this project's own palette builder (vq.scene_palette / H.build). The DIRECTION is measured and the magnitude is about right; if the packed player gets built, re-derive the per-frame number against the builder that will actually ship it.""") # --- the answer ---------------------------------------------------------- w9 = 9.0 free_packed_dma = PACKED_BPF * w9 + CHAIN_CLK + AUD_CLK free_packed_cpu = PACKED_BPF * w9 + blit[8] + AUD_CLK # ... and the same two rows with the PER-FRAME PALETTE actually charged, which # is what a player ships. The picture rows above are the comparison against the # codec and are left alone so the published 55.2% / 81.6% do not drift; these # are the shipping figures. Session 30 (FINDINGS 62) made the DMAC row's # version legal: the palette is a 193rd chain ENTRY, so it costs 512 more # delivered bytes and one more entry rather than 256 CPU word writes. pal_dma = (PACKED_BPF + PAL_BYTES) * w9 + CHAIN_CLK + B.DMA_CHAIN_CLK + AUD_CLK pal_cpu = (PACKED_BPF + PAL_BYTES) * w9 + blit[8] + pal_clk + AUD_CLK codec_9 = codec_bpf * w9 + codec_decode + AUD_CLK print("\n" + "=" * 78) print(f"""THE ANSWER, AT THE ONE RUNG THIS MACHINE CAN BE SHOWN TO RUN (W=9) CODEC, gate container {100*codec_9/FRAME_CLK:6.1f}% of the frame -- DOES NOT FIT free / DMAC->GVRAM / PACKED {100*free_packed_dma/FRAME_CLK:6.1f}% -- FITS, with {100-100*free_packed_dma/FRAME_CLK:.0f}% to spare free / CPU-painted / PACKED {100*free_packed_cpu/FRAME_CLK:6.1f}% -- FITS, with {100-100*free_packed_cpu/FRAME_CLK:.0f}% to spare WITH THE PER-FRAME PALETTE CHARGED, which is what would ship: DMAC-direct, palette on the CHAIN (62) {100*pal_dma/FRAME_CLK:6.1f}% of the frame, {(PACKED_BPF+PAL_BYTES)*FPS/1024:.0f} KB/s CPU-painted, palette written by the CPU {100*pal_cpu/FRAME_CLK:6.1f}% of the frame, {(PACKED_BPF+PAL_BYTES)*FPS/1024:.0f} KB/s The palette costs the same on the WIRE either way -- {PAL_BYTES} B a frame, +{100*PAL_BYTES/PACKED_BPF:.1f}% -- and the wire is where this design is expensive. The gap between the two rows is the PAINT, not the palette. What session 30 bought is smaller than either and is worth stating exactly: {pal_clk:,.0f} CPU clocks of palette writing replaced by one more chain entry at {B.DMA_CHAIN_CLK} clocks, a net {100*(pal_clk-B.DMA_CHAIN_CLK)/FRAME_CLK:.2f}% of a frame -- plus the structural half, which is that the video path then contains no per-frame PAINT at all. The CPU still issues the READ(10) and starts the channel, and neither of those is priced anywhere in this tree. THE DECODER-FREE PACKED PLAYER FITS THE CLOCK BUDGET THAT THE CODEC MISSES. That is not a small correction to 47.5, it is the reverse of the reason the codec exists. 44.7 said it in advance and on a different cost model: "the codec is not there to save CPU -- it is there to save the wire." The measured model agrees, and now says the CPU side is not merely affordable but strictly cheaper WITHOUT the codec: at the floor, decoding {codec_bpf:,.0f} bytes costs {100*(codec_bpf*w9+codec_decode)/FRAME_CLK:.0f}% of a frame and NOT decoding {PACKED_BPF:,} costs {100*(PACKED_BPF*w9+blit[8])/FRAME_CLK:.0f}%. SO THE QUESTION IS ENTIRELY A MEDIUM QUESTION, and it has two halves: 1. {PACKED_BPF*FPS/1024:.0f} KB/s SUSTAINED, with no lever to pull. ROADMAP B1 is unmeasured; the 0.7-1.7 MB/s usually quoted for BlueSCSI on an X68000 is folklore with no published benchmark behind it. {PACKED_BPF*FPS/1024:.0f} KB/s sits inside that range, which is exactly why the range has to be measured rather than cited. A codec at 327 KB/s survives a slower answer; a literal frame does not degrade, it drops. 2. {PACKED_BPF*FPS*GAME_S/1e9:.2f} GB for the whole game, against the codec's {codec_bpf*FPS*GAME_S/1e9:.2f} GB at the gate recipe and ~{327*1024*GAME_S/1e9:.2f} GB at E7's target. That is a packaging fact (C3), not a performance one. AND 47.4 STILL SITS OVER ALL OF IT. Everything above assumes the layer is visible while it is written. If it is not, the packed player is a {100*blit[8]/FRAME_CLK:.0f}% duty strobe at best and there is no version of it that is merely expensive.""")