NOTE: The Jupyter Notebook below is included in the Chimera SDK and can be run interactively by running the following CLI command:
$ quadric sdk notebook
From the Jupyter Notebook window in your browser, select the notebook named /quadric/sdk-cli/examples/games/doom/doom.ipynb.
DOOM Renderer on Chimera GPNPU

A full DOOM-style raycasting engine — DDA raycasting, textured walls, perspective-correct floors/ceilings, sprite compositing with z-buffering, and distance-based lighting — running as a single fused CCL (Chimera Compute Language) kernel on the Chimera GPNPU.
No matrix multiplies. No convolutions. Not a single GEMM. Every pixel rendered on-chip.
| Spec | Value |
|---|---|
| Resolution | 320x200 (native DOOM) |
| Kernel | Single EPU_ENTRY, ~800 lines CCL |
| Textures | Real DOOM WAD textures, 256-color indexed palette |
| Target | QC-N (8x8 core array, 8 MACs/PE) |
| OCM footprint | ~880 KB (texture atlases + framebuffer) |
| Per-frame host traffic | 32 bytes camera state + sprite list |
Why this matters
Carmack's renderer demands fixed-point integer arithmetic, data-dependent branching, irregular memory access patterns, and texture lookups through random-access tables — all under a hard frame-time deadline. This is the exact class of workload that traditional AI accelerator architectures cannot express. The Chimera GPNPU handles it natively because it was designed as a programmable computer, not a fixed-function accelerator.
Read the full blog post: Can Your NPU Run DOOM?
1. Setup
import os
import re
import numpy as np
from PIL import Image
from IPython.display import display
SRC_DIR = os.path.join(os.getcwd(), "src")
ASSET_DIR = os.path.join(os.getcwd(), "assets")
## Read resolution from the single source of truth
types_hpp = open(os.path.join(SRC_DIR, "types.hpp")).read()
if re.search(r"^\s*#define\s+DOOM_RES_224x168\s*$", types_hpp, re.MULTILINE):
SW, SH = 224, 168
else:
SW, SH = 320, 200
print(f"Resolution: {SW}x{SH} ({SW*SH:,} pixels, {SW//8}x{SH//8} = {(SW//8)*(SH//8):,} tiles)")
Resolution: 320x200 (64,000 pixels, 40x25 = 1,000 tiles)
2. Architecture
The renderer is a single EPU_ENTRY kernel with four phases:
- Load —
memCpycamera state + sprite data from DDR to OCM (On-Chip Memory). Texture atlases upload once and stay resident. - Column prepass — Cast one ray per screen column via batched DDA (Digital Differential Analysis). 32 map cells fetched in a single
rau::load::tilescall instead of 32 sequential round-trips. - Tile loop — For each 8x8 tile (1,000 total at 320x200), render floor/ceiling, composite walls, then sprites. Per-PE predication handles pixels that straddle the horizon or a wall/floor boundary.
- Output —
memCpythe OCM framebuffer back to DDR.
Key architectural features exercised:
- Batched RAU random-access loads — every PE needs a different texel from the atlas.
rau::load::tilesfetches per-PE addresses in one flow. - Indexed color with palette in LRM — 8-bit texel indices keep atlases small; the 256-entry DOOM palette lives as a LUT in LRM (Local Register Memory), the per-PE register file.
- SIMD predication —
chimera::anyOf()early-exits sprite compositing when no PE in the tile overlaps a sprite. Per-PE ternary selects composite wall over floor, sprite over wall. - Hardware fixed-point divide — floor/ceiling depth uses
math::fxDivhardware divide intrinsic instead of software division.
Data-Movement Map
═══════════════════════════════════════════════════════════════════
DOOM RENDERER — QC-N (8×8 core array, 64 PEs per tile)
Screen: 320×200 → 40×25 = 1,000 tiles
═══════════════════════════════════════════════════════════════════
HOST (DDR) OCM (4MB)
────────── ─────────
┌──────────────────┐ memCpy ┌──────────────────────────┐
│ Camera State │──────────────→ │ ocmState [1×8 i32] │
│ [32 bytes] │ every frame │ │
├──────────────────┤ ├──────────────────────────┤
│ Sprite Counts │──────────────→ │ ocmSpriteCounts │
│ [25×40 i32] │ every frame │ [25×40 i32] │
├──────────────────┤ ├──────────────────────────┤
│ Sprite Data │──────────────→ │ ocmSprites │
│ [4000×12 i32] │ every frame │ [4000×12 i32] │
├──────────────────┤ ├──────────────────────────┤
│ Map [16×16 i32] │──────────────→ │ ocmMap [16×16 i32] │
├──────────────────┤ first frame ├──────────────────────────┤
│ Wall Atlas │──────────────→ │ ocmWallTex │
│ [1024×512 u8] │ only │ [1024×512 u8] = 512KB │
├──────────────────┤ ├──────────────────────────┤
│ Flat Atlas │──────────────→ │ ocmFlatTex │
│ [384×256 u8] │ │ [384×256 u8] = 96KB │
├──────────────────┤ ├──────────────────────────┤
│ Sprite Atlas │──────────────→ │ ocmSpriteTex │
│ [576×256 u8] │ │ [576×256 u8] = 144KB │
├──────────────────┤ ├──────────────────────────┤
│ Framebuffer │ ←──────────── │ ocmFb │
│ [200×320 u16] │ memCpy out │ [200×320 u16] = 128KB │
└──────────────────┘ └──────────────────────────┘
LRM (per-PE registers)
──────────────────────
┌──────────────────────────────────┐
│ paletteCache [256 × i32] │ DOOM palette as LUT
│ localCols [40 × 10 × i32] │ column prepass results
│ camera state (px,py,dir,plane) │ qVar registers
└──────────────────────────────────┘
═══════════════════════════════════════════════════════════════════
ALGORITHM FLOW
═══════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────┐
│ PHASE 1: LOAD │
│ │
│ DDR ──memCpy──→ OCM (state, sprites every frame) │
│ DDR ──memCpy──→ OCM (map + 3 atlases, first frame only) │
│ │
│ OCM ──rau::load::oneTile──→ LRM (8 camera fields) │
│ generated_assets.hpp ──→ LRM (256-entry palette LUT) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PHASE 2: COLUMN PREPASS (tileX = 0..39 → pixels 0..319) │
│ │
│ For each of 40 columns: │
│ ┌───────────────────────────────────────────────┐ │
│ │ castRay(): │ │
│ │ │ │
│ │ Step 1: 32 DDA steps, pure ALU │ │
│ │ compute 32 map cell addresses │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Step 2: rau::load::tiles (32 addrs, 1 flow) │ │
│ │ OCM ocmMap ──→ 32 cell values │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Step 3: scan for first hit — per-PE predicate │ │
│ │ perpDist = firstHit ? dist : perpDist │ │
│ │ → perpDist, wallTop/Bot, texU, shade │ │
│ └───────────────────────────────────────────────┘ │
│ Store 10 fields → localCols[tileX] (LRM registers) │
│ │
│ Each tileX covers 8 PEs = 8 adjacent screen columns │
│ tileX=0 → px 0..7, tileX=1 → px 8..15, ... tileX=39 → px 312..319
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PHASE 3: TILE LOOP │
│ tileY = 0..24 → rows 0..7, 8..15, ... 192..199 │
│ tileX = 0..39 → cols 0..7, 8..15, ... 312..319 │
│ = 1,000 tiles, 64 PEs each, each PE = one screen pixel │
│ │
│ Per tile row (25×): │
│ buildFloorRowSample() ── LUT in LRM (no division) │
│ buildCeilingRowSample() │
│ │
│ Per tile (1000×): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─ FLOOR/CEILING ─────────────────────────────┐ │ │
│ │ │ if allFloor/allCeil: skip other plane │ │ │
│ │ │ else: compute both, per-PE predicate select │ │ │
│ │ │ addr = isFloor ? floorAddr : ceilAddr │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ rau::load::tiles (1 addr per PE, 1 flow) │ │ │
│ │ │ OCM ocmFlatTex ──→ palette index │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ paletteCache[idx] → shade → RGB565 pixel │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─ WALL ──────────────────────────────────────┐ │ │
│ │ │ read wallTop/Bot/texU/shade from localCols │ │ │
│ │ │ compute per-PE texel addr │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ rau::load::tiles (1 addr per PE, 1 flow) │ │ │
│ │ │ OCM ocmWallTex ──→ palette index │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ pixel = isWall ? wallPixel : floorPixel │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─ SPRITES (up to 4 per tile) ───────────────┐ │ │
│ │ │ rau::load::oneTile → spriteCount │ │ │
│ │ │ │ │ │
│ │ │ Per sprite: │ │ │
│ │ │ rau::load::tiles (12 addrs, 1 flow) │ │ │
│ │ │ OCM ocmSprites ──→ bounds,UV,depth,shade │ │ │
│ │ │ │ │ │ │
│ │ │ if !anyOf(inSprite): continue ◄── early out│ │ │
│ │ │ │ │ │ │
│ │ │ rau::load::tiles (1 addr per PE, 1 flow) │ │ │
│ │ │ OCM ocmSpriteTex ──→ palette index │ │ │
│ │ │ │ │ │ │
│ │ │ pixel = sprCloser ? sprPixel : pixel │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─ WRITEBACK ─────────────────────────────────┐ │ │
│ │ │ WriteFlow<TileAccess> (MinRoiDescriptor) │ │ │
│ │ │ 64 PEs write 64 pixels → ocmFb │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PHASE 4: OUTPUT │
│ │
│ OCM ocmFb ──memCpy──→ DDR ddrFb [200×320 u16 = 128KB] │
└─────────────────────────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════
DATA MOVEMENT SUMMARY
═══════════════════════════════════════════════════════════════════
Type │ Pattern │ Per frame
──────────────────┼──────────────────────┼──────────────────
DDR→OCM memCpy │ bulk DMA │ 32B state + sprites
DDR→OCM memCpy │ bulk DMA │ ~880KB first frame
OCM→LRM rau │ oneTile (scalar) │ 8 camera fields
OCM→LRM rau │ load::tiles (batched)│ 40 × 32-addr (ray)
OCM→LRM rau │ load::tiles (batched)│ 1000 × 1-addr (flat)
OCM→LRM rau │ load::tiles (batched)│ 1000 × 1-addr (wall)
OCM→LRM rau │ load::tiles (batched)│ ≤4000 × 13-addr (spr)
LRM→OCM WriteFlow │ MinRoi (tile) │ 1000 tiles
OCM→DDR memCpy │ bulk DMA │ 128KB framebuffer
Build & Run
make # compile and run renderer on ISS
make play # interactive game (requires pygame)
make headless # render 3 test views, no display
make clean # remove build artifacts
Resolution Toggle
Edit src/types.hpp — uncomment #define DOOM_RES_224x168 for the smaller resolution, or leave it commented for native 320x200. Both the C++ kernel and Python game read from this single source of truth.
| Resolution | Tiles | Cycles (QC-N ISS) |
|---|---|---|
| 320x200 (native DOOM) | 40x25 = 1,000 | ~735K |
| 224x168 | 28x21 = 588 | ~458K |
Controls
WASD: move, Arrow keys: turn, SPACE: shoot, SHIFT: run, TAB: minimap, ESC: quit
3. The CCL Renderer Kernel
The kernel is structured across these files:
| File | Purpose |
|---|---|
renderer.cpp | Main kernel (EPU_ENTRY) + host test harness |
types.hpp | Tensor types, screen geometry, resolution toggle |
ray.hpp | Batched DDA raycasting (32 steps, 1 RAU flow) |
floor.hpp | Floor/ceiling perspective sampling |
wall.hpp | Wall texture shading |
sprite.hpp | Sprite compositing with anyOf() early-exit |
texture.hpp | Atlas address computation, palette lookup |
lighting.hpp | 4-directional linalg::dot lighting + fog |
generated_assets.hpp | Atlas dimensions, DOOM palette (auto-generated) |
Let's look at the kernel entry point:
## Show the kernel signature and phase structure
renderer_src = open(os.path.join(SRC_DIR, "renderer.cpp")).read()
## Extract just the kernel function (EPU_ENTRY to the end of the kernel)
start = renderer_src.find("EPU_ENTRY")
end = renderer_src.find("HOST_MAIN")
kernel_code = renderer_src[start:end].strip()
print(f"Kernel: {len(kernel_code.splitlines())} lines\n")
## Show first 40 lines (setup + phase 1)
for i, line in enumerate(kernel_code.splitlines()[:40], 1):
print(f"{i:3d} {line}")
Kernel: 272 lines
1 EPU_ENTRY void renderFrame(
2 std::int32_t loadStaticFlag,
3 DdrState::ptrType statePtr,
4 DdrMap::ptrType mapPtr,
5 DdrSpriteCounts::ptrType spriteCountsPtr,
6 DdrSprites::ptrType spritesPtr,
7 DdrWallTextures::ptrType wallTexPtr,
8 DdrFlatTextures::ptrType flatTexPtr,
9 DdrSpriteTextures::ptrType spriteTexPtr,
10 DdrScreen::ptrType fbPtr
11 ) {
12 DdrState ddrState(statePtr);
13 DdrMap ddrMap(mapPtr);
14 DdrSpriteCounts ddrSpriteCounts(spriteCountsPtr);
15 DdrSprites ddrSprites(spritesPtr);
16 DdrWallTextures ddrWallTex(wallTexPtr);
17 DdrFlatTextures ddrFlatTex(flatTexPtr);
18 DdrSpriteTextures ddrSpriteTex(spriteTexPtr);
19 DdrScreen ddrFb(fbPtr);
20
21 MemAllocator ocmMem;
22 OcmState ocmState; ocmMem.allocate(ocmState);
23 OcmMap ocmMap; ocmMem.allocate(ocmMap);
24 OcmSpriteCounts ocmSpriteCounts; ocmMem.allocate(ocmSpriteCounts);
25 OcmSprites ocmSprites; ocmMem.allocate(ocmSprites);
26 OcmWallTextures ocmWallTex; ocmMem.allocate(ocmWallTex);
27 OcmFlatTextures ocmFlatTex; ocmMem.allocate(ocmFlatTex);
28 OcmSpriteTextures ocmSpriteTex; ocmMem.allocate(ocmSpriteTex);
29 OcmScreen ocmFb; ocmMem.allocate(ocmFb);
30
31 DOOM_PROFILE_START("region: 1 / 8 state+static_load");
32 memCpy(ddrState, ocmState);
33 memCpy(ddrSpriteCounts, ocmSpriteCounts);
34 memCpy(ddrSprites, ocmSprites);
35 if(loadStaticFlag != 0) {
36 memCpy(ddrMap, ocmMap);
37 memCpy(ddrWallTex, ocmWallTex);
38 memCpy(ddrFlatTex, ocmFlatTex);
39 memCpy(ddrSpriteTex, ocmSpriteTex);
40 }
4. Compile & Run
The kernel compiles with sdk source — one command handles the full LLVM pipeline (CCL → Quadric LLVM → EPU binary) and runs on the ISS (Instruction Set Simulator).
!make renderer
sdk source src/renderer.cpp --target QC-N --ocm-size 4MB --macs-per-pe 8
[32m[SDK-CLI] : Compiling source[0m
2026-06-19 03:53 - DEBUG - sdk - cli - Executing command: cmake CMakeLists.txt -B /quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build -DNUM_GPNPUS=1 -DNUM_CORES=8 -DNUM_BORDERS=2 -DEPU_VERSION=2.0.0 -DQLLVM_ROOT_PATH=/quadric/llvm -DOCM_SIZE_KIBIBYTES=4096 -DNUM_PE_MACS=8 -DASSERT_MLS_WIDTH_LINE_ALIGN=ON -DHARDWARE_TARGET=OFF
[0mCMAKE_CXX_COMPILER: /quadric/llvm/bin/clang++[0m
-- The C compiler identification is Clang 8.0.1
-- The CXX compiler identification is Clang 8.0.1
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /quadric/llvm/bin/clang - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /quadric/llvm/bin/clang++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.2s)
-- Generating done (0.0s)
-- Build files have been written to: /quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build
2026-06-19 03:53 - DEBUG - sdk - cli - Executing command: make -j8
clang-8: [0;1;35mwarning: [0m/quadric/llvm/bin/clang++: 'linker' input unused [-Wunused-command-line-argument][0m
[0mrenderer_epu: EPU opt stage[0m
[0mrenderer_epu: EPU .s to elf[0m
In file included from /quadric/sdk-cli/examples/games/doom/src/renderer.cpp:23:
In file included from /quadric/sdk-cli/examples/games/doom/src/ray.hpp:13:
[1m/quadric/sdk-cli/examples/games/doom/src/lighting.hpp:15:29: [0m[0;1;35mwarning: [0m[1mloop not
unrolled: the optimizer was unable to perform the requested
transformation; the transformation might be disabled or specified as part
of an unsupported transformation ordering
[-Wpass-failed=transform-warning][0m
INLINE qVar_t<std::int32_t> computeLighting(
[0;1;32m ^
[0m[1m/quadric/sdk-cli/examples/games/doom/src/lighting.hpp:15:29: [0m[0;1;35mwarning: [0m[1mloop not
unrolled: the optimizer was unable to perform the requested
transformation; the transformation might be disabled or specified as part
of an unsupported transformation ordering
[-Wpass-failed=transform-warning][0m
[1m/quadric/sdk-cli/examples/games/doom/src/lighting.hpp:15:29: [0m[0;1;35mwarning: [0m[1mloop not
unrolled: the optimizer was unable to perform the requested
transformation; the transformation might be disabled or specified as part
of an unsupported transformation ordering
[-Wpass-failed=transform-warning][0m
[1m/quadric/sdk-cli/examples/games/doom/src/lighting.hpp:15:29: [0m[0;1;35mwarning: [0m[1mloop not
unrolled: the optimizer was unable to perform the requested
transformation; the transformation might be disabled or specified as part
of an unsupported transformation ordering
[-Wpass-failed=transform-warning][0m
In file included from /quadric/sdk-cli/examples/games/doom/src/renderer.cpp:23:
[1m/quadric/sdk-cli/examples/games/doom/src/ray.hpp:15:15: [0m[0;1;35mwarning: [0m[1mloop not
unrolled: the optimizer was unable to perform the requested
transformation; the transformation might be disabled or specified as part
of an unsupported transformation ordering
[-Wpass-failed=transform-warning][0m
INLINE RayHit castRay(
[0;1;32m ^
[0m[1m/quadric/sdk-cli/examples/games/doom/src/ray.hpp:15:15: [0m[0;1;35mwarning: [0m[1mloop not
unrolled: the optimizer was unable to perform the requested
transformation; the transformation might be disabled or specified as part
of an unsupported transformation ordering
[-Wpass-failed=transform-warning][0m
[1m/quadric/sdk-cli/examples/games/doom/src/ray.hpp:15:15: [0m[0;1;35mwarning: [0m[1mloop not
unrolled: the optimizer was unable to perform the requested
transformation; the transformation might be disabled or specified as part
of an unsupported transformation ordering
[-Wpass-failed=transform-warning][0m
[1m/quadric/sdk-cli/examples/games/doom/src/ray.hpp:15:15: [0m[0;1;35mwarning: [0m[1mloop not
unrolled: the optimizer was unable to perform the requested
transformation; the transformation might be disabled or specified as part
of an unsupported transformation ordering
[-Wpass-failed=transform-warning][0m
8 warnings generated.
make[1]: Entering directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
make[2]: Entering directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
make[3]: Entering directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
make[3]: Entering directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
make[3]: Leaving directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
make[3]: Leaving directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
make[3]: Entering directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
make[3]: Entering directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
[ 25%] [32mBuilding CXX object CMakeFiles/renderer_epu.dir/quadric/sdk-cli/examples/games/doom/src/renderer.cpp.o[0m
[ 50%] [32mBuilding CXX object CMakeFiles/renderer_host.dir/quadric/sdk-cli/examples/games/doom/src/renderer.cpp.o[0m
[ 75%] [32m[1mLinking EPU executable /quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/output/renderer_epu.s[0m
make[3]: Leaving directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
[ 75%] Built target renderer_epu
[100%] [32m[1mLinking CXX executable /quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/output/renderer_host[0m
make[3]: Leaving directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
[100%] Built target renderer_host
make[2]: Leaving directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
make[1]: Leaving directory '/quadric/sdk-cli/examples/games/doom/renderer_QC-N_4MB_4kB_128GBps_128GBps/build'
[32m[SDK-CLI] : Executing on QC-N simulator[0m
2026-06-19 03:53 - DEBUG - sdk - cli - Executing command: ./renderer_host -c --ddrRdBwTotal 1048576.0 --ddrWrBwTotal 1048576.0 --ddrAxiWidth 128 --instMemDepth 1310720 --ocmSize 4194304 --cycleTimeNS 0.5882352941176471 --no-check --ddrRdAvgPct 100 --ddrRdMaxPct 100 --ddrWrAvgPct 100 --ddrWrMaxPct 100 --postKernelFlowTimeoutCycles 4000000 --clusterSize 1 --numClusters 1
seed: 2942817044
Fused renderer: camera=(8,5) angle=0.0, 2 enemies
Global seed is 2942817044
[PROFILE_KGVCID_{00:00}] Total execution time (cycles): 637931
------------------------------------------------
[PROFILE_KGVCID_{00:00}] Profile: default
[PROFILE_KGVCID_{00:00}] TotalCycles = 637931
[PROFILE_KGVCID_{00:00}] ExecCycles[COMPUTE] = 132111
[PROFILE_KGVCID_{00:00}] ExecCycles[MAC] = 0
[PROFILE_KGVCID_{00:00}] ExecCycles[MAC_REDUCE] = 0
[PROFILE_KGVCID_{00:00}] ExecCycles[COMPARE] = 17329
[PROFILE_KGVCID_{00:00}] ExecCycles[BRANCH] = 8834
[PROFILE_KGVCID_{00:00}] ExecCycles[CONDITIONAL] = 10346
[PROFILE_KGVCID_{00:00}] ExecCycles[ITERATIVE] = 0
[PROFILE_KGVCID_{00:00}] ExecCycles[PREDICATION] = 7120
[PROFILE_KGVCID_{00:00}] ExecCycles[CONSTANT_MATERIALIZATION] = 32502
[PROFILE_KGVCID_{00:00}] ExecCycles[FLOW_SETUP] = 35606
[PROFILE_KGVCID_{00:00}] ExecCycles[LOOP_SETUP] = 81
[PROFILE_KGVCID_{00:00}] ExecCycles[REG_MOVEMENT] = 54576
[PROFILE_KGVCID_{00:00}] ExecCycles[DATA_MOVEMENT] = 8296
[PROFILE_KGVCID_{00:00}] ExecCycles[KERNEL_SETUP] = 13
[PROFILE_KGVCID_{00:00}] ExecCycles[INTERRUPT] = 1
[PROFILE_KGVCID_{00:00}] StallCycles[DIV] = 9300
[PROFILE_KGVCID_{00:00}] StallCycles[MODE_WRITE] = 16
[PROFILE_KGVCID_{00:00}] StallCycles[SPRF_RAW] = 81
[PROFILE_KGVCID_{00:00}] StallCycles[SPRF_RARMW] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[SPRF_PWARMW] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[DEREF_REGPTR_CONFLICT] = 1792
[PROFILE_KGVCID_{00:00}] StallCycles[BRANCH] = 17387
[PROFILE_KGVCID_{00:00}] StallCycles[LOAD] = 238755
[PROFILE_KGVCID_{00:00}] StallCycles[STORE] = 30
[PROFILE_KGVCID_{00:00}] StallCycles[EXT_LOAD] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[EXT_STORE] = 8115
[PROFILE_KGVCID_{00:00}] StallCycles[MLS_SEND] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[RAU] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[PREDICATION] = 6000
[PROFILE_KGVCID_{00:00}] StallCycles[MUL] = 10610
[PROFILE_KGVCID_{00:00}] StallCycles[FX32TOFP16] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[MAC_REDUCE] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[NBR_RAW] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[MEU] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[PATCH_MESH] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[QLS_Q_FULL] = 33206
[PROFILE_KGVCID_{00:00}] StallCycles[BAR_LOAD] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[BAR_STORE] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[BAR_BUBBLE] = 4048
[PROFILE_KGVCID_{00:00}] StallCycles[BARRIER_MLS_DONE] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[INTERRUPT] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[TENSOR_TABLE_FULL] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[MLS_FIFO_FULL] = 0
[PROFILE_KGVCID_{00:00}] StallCycles[INSTRUCTION_FETCH] = 1775
------------------------------------------------
[PROFILE_KGVCID_{00:00}] ExtBytes[LOAD] = 967104
[PROFILE_KGVCID_{00:00}] ExtBytes[STORE] = 128000
Saved renderer_frame.ppm
[32m[SDK-CLI] : TotalCycles: 637,931[0m
[32m[SDK-CLI] : Executions/second: 2,665
[0m
[0mcompute : [0m[96m▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇[0m 294.952K
data_array : [0m[96m▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇[0m 96.078K
mac : [0m[90m[96m[0m 0
data_ocm : [0m[96m▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇[0m 238.785K
data_external: [0m[96m▇[0m 8.115K
[32m[SDK-CLI] : Execution completed.[0m
[0m
5. Visualize the Rendered Frame
The ISS writes a .ppm framebuffer alongside the compiled kernel.
import glob
ppm_candidates = glob.glob("**/renderer_frame.ppm", recursive=True)
if ppm_candidates:
frame = Image.open(ppm_candidates[0])
scaled = frame.resize((frame.width * 3, frame.height * 3), Image.NEAREST)
display(scaled)
print(f"Frame: {frame.width}x{frame.height}, rendered on Chimera GPNPU (ISS)")
else:
print("No rendered frame found. Run the compile step first.")

Frame: 320x200, rendered on Chimera GPNPU (ISS)
previews = [
("Wall Textures", "doom_wall_preview.png"),
("Floor/Ceiling Flats", "doom_flat_preview.png"),
("Sprite Sheet", "doom_sprite_preview.png"),
]
for title, fname in previews:
path = os.path.join(ASSET_DIR, fname)
if os.path.exists(path):
img = Image.open(path)
print(f"\n{title} ({img.width}x{img.height}):")
display(img)
Wall Textures (1024x1024):

Floor/Ceiling Flats (256x384):

Sprite Sheet (256x576):

Summary
| Metric | Value |
|---|---|
| Resolution | 320x200 (native DOOM) |
| Kernel size | ~800 lines CCL |
| Parameters | 0 (no neural network) |
| GMACs | 0 |
| OCM footprint | ~880 KB |
| Per-frame DDR traffic | 32B in + 128KB out |
| Technique | Why |
|---|---|
| Batched RAU loads | 32 DDA map lookups in 1 flow instead of 32 |
| Indexed color + LRM palette | 1 byte/texel, palette lookup is free |
anyOf() sprite early-exit | Skip texture fetch when no PE overlaps |
math::fxDiv hardware divide | Floor/ceiling depth without software division |
| Static OCM residency | Textures upload once, stay on-chip |
| Per-PE predication | Wall/floor/ceiling/sprite compositing without branching |
Key Takeaways
- The Chimera GPNPU runs a complete raycasting renderer as a single kernel — no graph compiler, no operator library, just C++.
- RAU random-access loads are what make texture mapping possible on a tile-based SIMD array.
- The same architectural properties that enable this (CCL programmability, RAU, OCM autonomy, mega-kernel execution) are what close the workload gap for real-world edge AI: custom operators, sensor fusion, pre/post-processing.
Blog post: Can Your NPU Run DOOM?