diff --git a/CLAUDE.md b/CLAUDE.md index 14f8357..c90db8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # Blit-Tech -Lightweight WebGPU retro engine for TypeScript, inspired by RetroBlit. Pixel-perfect 2D rendering with a -fantasy-console-style API. +A palette-first WebGPU retro engine for TypeScript, inspired by RetroBlit. Pixel-perfect 2D rendering where primitives +and sprites resolve through a shared indexed palette. ## Tech Stack @@ -24,7 +24,8 @@ Before writing new code, reviewing existing code, or preflighting, check here fi | What does `BT.X()` do? | `src/BlitTech.ts` JSDoc, then `docs/api-*.md` | | How does a subsystem work internally? | The relevant `src/core/` or `src/render/` file | | What does a demo implement? | `src/core/IBlitTechDemo.ts` (interface + HardwareSettings) | -| What palette/sprite setup pattern is correct? | `docs/api-assets.md`, then `docs/api-palette.md` | +| What palette/sprite setup pattern is correct? | `docs/palette-guide.md`, then `docs/api-assets.md` | +| Which preset has which exact color values? | `docs/palette-presets.md` | | How do post-process effects work? | `docs/post-process-effects.md` | | What does the CI do on this file? | `.github/workflows/ci.yml` | | What is the benchmark threshold? | `ci.yml` benchmark job (`--threshold 25` flag), not docs | @@ -35,8 +36,9 @@ Before writing new code, reviewing existing code, or preflighting, check here fi ## Architecture -All engine functionality is accessed through the static `BT` namespace. Demos implement the `IBlitTechDemo` interface -(`configure?`, `init`, `update`, `render`). +All engine functionality is accessed through the static `BT` namespace. The architecture is palette-first: primitives, +sprites, and bitmap text resolve color through the active `Palette` before final RGBA output. Demos implement the +`IBlitTechDemo` interface (`configure?`, `init`, `update`, `render`). ```text src/ @@ -89,11 +91,11 @@ src/ setup.ts # Vitest global setup (GPU constants) ``` -### Rendering +### Palette-First Rendering Two backends selectable via `HardwareSettings.renderer` (default `'webgpu'`): -- **WebGPU** (`'webgpu'`): dual-pipeline hardware renderer. +- **WebGPU** (`'webgpu'`): indexed, palette-first hardware renderer. 1. **Primitives pipeline** - batched geometry writing **palette indices** (pixels, lines, rects). Max 50k vertices/frame. 2. **Sprites pipeline** - batched **palette-indexed** textured quads (sprites, bitmap text). Max 50k vertices (~8333 @@ -106,7 +108,7 @@ Two backends selectable via `HardwareSettings.renderer` (default `'webgpu'`): blits, and bitmap text. Post-process/fullscreen effects throw a clear error directing users to the WebGPU backend. Activates automatically when WebGPU init fails; force explicitly via `HardwareSettings.renderer: 'software'` or the `?renderer=software` URL query parameter. A dismissible in-canvas ticker banner is rendered each frame when this - backend is active. Use `BTAPI.getActiveBackend()` to query which backend started (`'webgpu' | 'software' | null`). + backend is active. Use `BT.getActiveBackend()` to query which backend started (`'webgpu' | 'software' | null`). ### Core Types diff --git a/README.md b/README.md index 84aaf9c..b231cbc 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ [![WebGPU](https://img.shields.io/badge/WebGPU-Enabled-green.svg)](https://www.w3.org/TR/webgpu/) [![pnpm](https://img.shields.io/badge/pnpm-10.26.2-yellow.svg)](https://pnpm.io/) -A lightweight WebGPU retro engine for TypeScript, inspired by [RetroBlit](https://badcastle.itch.io/retroblit). Build -pixel-perfect 2D demos with a clean, fantasy-console-style API. +A palette-first WebGPU retro engine for TypeScript, inspired by [RetroBlit](https://badcastle.itch.io/retroblit). Draw +with palette indices, animate with palette cycling and fades, and ship authentic VGA-era effects on modern GPUs. ![Blit-Tech logo](assets/logo.png) @@ -19,9 +19,12 @@ complex frameworks, just sprites, primitives, and fonts. ## Features +- **True indexed rendering**: primitives and sprites write palette indices, not RGBA pixels +- **Palette effects built-in**: cycling, fade, flash, and swap run per frame with no per-sprite rewrites +- **Built-in retro palettes**: VGA, CGA, C64, Game Boy, PICO-8, and NES preset factories +- **Palette offset variants**: recolor one sprite sheet into team colors, states, or themes without duplicate textures +- **Performance-first data model**: tiny palette uploads (4 KB), smaller sprite textures, and compact primitive vertices - **WebGPU rendering** with dual-pipeline architecture (primitives + sprites); automatic Canvas 2D software fallback -- **Palette system**: 256-entry indexed color palette with built-in presets (VGA, CGA, C64, Game Boy, PICO-8, NES) -- **Palette effects**: cycling, fade, flash, swap with easing functions — animated each frame - **Post-process effects**: two-tier system — pixel tier on the `r8uint` index framebuffer; display tier on upscaled RGBA; bundled CRT presets - **Primitive drawing**: pixels, lines, rectangles (outline and filled) @@ -35,6 +38,16 @@ complex frameworks, just sprites, primitives, and fonts. - **Fixed timestep**: deterministic update loop with tick counter, `Timer`, and timing helpers - **Frame capture**: `BT.captureFrame()` and `BT.downloadFrame()` for PNG export +## Why Blit-Tech? + +| Feature | Blit-Tech | Typical 2D WebGPU engines | +| ------------------------ | ----------------------------------- | -------------------------------- | +| Rendering model | Native indexed palette pipeline | RGBA textures and framebuffers | +| Color animation | Palette cycling/fade/flash built-in | Manual sprite or shader rewrites | +| Global recolor/fade cost | One palette update | Scene redraw and blend passes | +| Color variants | Palette offsets | Duplicate assets or tint logic | +| Retro palette presets | C64, NES, Game Boy, CGA, VGA, etc. | Usually custom/manual only | + ## Prerequisites - **Node.js** v22 or higher (LTS) @@ -65,17 +78,24 @@ For interactive examples and demos, visit the [Blit-Tech Demos repository](https ## Quick Start ```ts -import { bootstrap, BT, Color32, Palette, Rect2i, Vector2i, type IBlitTechDemo } from 'blit-tech'; +import { bootstrap, BT, Color32, Palette, Rect2i, type IBlitTechDemo } from 'blit-tech'; const BG = 1; -const RED = 2; +const WATER_A = 9; +const WATER_B = 12; class MyDemo implements IBlitTechDemo { async init(): Promise { - const palette = new Palette(16); + const palette = Palette.c64(); palette.set(BG, new Color32(20, 30, 40, 255)); - palette.set(RED, new Color32(255, 100, 50, 255)); BT.paletteSet(palette); + + // Animate every pixel that uses slots 9..12 (water/lava style cycling). + BT.paletteCycle(WATER_A, WATER_B, 6); + + // Quick full-screen impact flash via palette manipulation. + BT.paletteFlash(Color32.white, 120); + return true; } @@ -85,7 +105,7 @@ class MyDemo implements IBlitTechDemo { render(): void { BT.clear(BG); - BT.drawRectFill(new Rect2i(100, 100, 50, 50), RED); + BT.drawRectFill(new Rect2i(100, 100, 50, 50), WATER_A); } } @@ -132,6 +152,8 @@ bootstrap(MyDemo); | [API: Core](docs/api-core.md) | bootstrap, init, game loop, camera, Timer, core types | | [API: Rendering](docs/api-rendering.md) | primitives, sprites, text, post-process, frame capture | | [API: Palette](docs/api-palette.md) | palette setup, presets, effects, serialization | +| [Palette Guide](docs/palette-guide.md) | palette-first workflow, offsets, effects, performance | +| [Palette Presets](docs/palette-presets.md) | built-in preset reference and exact color data | | [API: Assets](docs/api-assets.md) | sprite sheets, bitmap fonts, asset loading | | [Input Guide](docs/input.md) | pointer, keyboard, gamepad | | [Post-Process Effects](docs/post-process-effects.md) | effect chain, built-in effects, custom effects | diff --git a/docs/api-assets.md b/docs/api-assets.md index b7bad7c..f421d1d 100644 --- a/docs/api-assets.md +++ b/docs/api-assets.md @@ -126,9 +126,11 @@ Use `BT.systemPrint()` for debug overlays and simple HUDs. For styled variable-w ## See Also -| Guide | What it covers | -| ---------------------------------- | --------------------------------------- | -| [API: Core](api-core.md) | bootstrap, init, game loop, core types | -| [API: Rendering](api-rendering.md) | primitives, sprites, text, post-process | -| [API: Palette](api-palette.md) | palette setup, presets, effects | -| [Bitmap Fonts](bitmap-fonts.md) | .btfont format, BMFont conversion | +| Guide | What it covers | +| ------------------------------------- | --------------------------------------- | +| [API: Core](api-core.md) | bootstrap, init, game loop, core types | +| [API: Rendering](api-rendering.md) | primitives, sprites, text, post-process | +| [API: Palette](api-palette.md) | palette setup, presets, effects | +| [Palette Guide](palette-guide.md) | palette-first setup, offsets, refresh | +| [Palette Presets](palette-presets.md) | built-in preset reference | +| [Bitmap Fonts](bitmap-fonts.md) | .btfont format, BMFont conversion | diff --git a/docs/api-core.md b/docs/api-core.md index bc76481..156a170 100644 --- a/docs/api-core.md +++ b/docs/api-core.md @@ -217,6 +217,7 @@ c.toHex() // '#rrggbb' string | ---------------------------------- | ------------------------------------------------------ | | [API: Rendering](api-rendering.md) | primitives, sprites, text, post-process, frame capture | | [API: Palette](api-palette.md) | palette setup, presets, effects | +| [Palette Guide](palette-guide.md) | palette-first workflow and practical patterns | | [API: Assets](api-assets.md) | sprite sheets, bitmap fonts, asset loading | | [Input Guide](input.md) | pointer, keyboard, gamepad | | [Testing](testing.md) | test tiers, WebGPU mocks | diff --git a/docs/api-palette.md b/docs/api-palette.md index ea8bf7d..7b240e5 100644 --- a/docs/api-palette.md +++ b/docs/api-palette.md @@ -161,8 +161,11 @@ Effects that auto-remove (fade, flash) clean up when their duration elapses. `pa ## See Also -| Guide | What it covers | -| ---------------------------------- | ------------------------------------------ | -| [API: Core](api-core.md) | bootstrap, init, game loop, core types | -| [API: Rendering](api-rendering.md) | primitives, sprites, text, post-process | -| [API: Assets](api-assets.md) | sprite sheets, bitmap fonts, asset loading | +| Guide | What it covers | +| ------------------------------------- | ------------------------------------------ | +| [API: Core](api-core.md) | bootstrap, init, game loop, core types | +| [API: Rendering](api-rendering.md) | primitives, sprites, text, post-process | +| [API: Assets](api-assets.md) | sprite sheets, bitmap fonts, asset loading | +| [Palette Guide](palette-guide.md) | end-to-end palette workflow and best usage | +| [Palette Presets](palette-presets.md) | exact built-in palette and HUD color data | +| [Testing](testing.md) | test tiers and palette testing patterns | diff --git a/docs/api-rendering.md b/docs/api-rendering.md index 5a1f9dc..d3d799f 100644 --- a/docs/api-rendering.md +++ b/docs/api-rendering.md @@ -170,6 +170,7 @@ await BT.downloadFrame('screenshot-001.png'); // custom filename | ----------------------------------------------- | ------------------------------------------ | | [API: Core](api-core.md) | bootstrap, init, camera, core types | | [API: Palette](api-palette.md) | palette setup, presets, effects | +| [Palette Guide](palette-guide.md) | palette-first workflow and offset patterns | | [API: Assets](api-assets.md) | sprite sheets, bitmap fonts, asset loading | | [Post-Process Effects](post-process-effects.md) | effect chain, custom effects | | [Bitmap Fonts](bitmap-fonts.md) | .btfont format, BMFont conversion | diff --git a/docs/palette-guide.md b/docs/palette-guide.md new file mode 100644 index 0000000..7a0474d --- /dev/null +++ b/docs/palette-guide.md @@ -0,0 +1,180 @@ +# Palette Guide + +Blit-Tech is palette-first: every visible pixel stores a palette slot index, and final RGB color comes from the active +`Palette`. Changing palette data changes every pixel that references those slots. + +This guide covers the end-to-end workflow: setup, indexed sprites, palette offsets, runtime effects, and when to call +`BT.spritesRefresh()`. + +--- + +## 1) Create and activate a palette + +```ts +import { BT, Color32, Palette } from 'blit-tech'; + +const palette = Palette.c64(); // or new Palette(256) +palette.set(1, new Color32(20, 30, 40, 255)); // custom background slot +BT.paletteSet(palette); +``` + +Key rules: + +- Slot `0` is always transparent. +- Valid sizes are `2, 4, 16, 32, 64, 128, 256`. +- `BT.paletteSet()` makes one palette active for all drawing. + +--- + +## 2) Draw using palette indices + +Draw calls accept numeric palette slots, not direct RGBA values: + +```ts +BT.clear(1); +BT.drawRectFill(rect, 6); +BT.drawLine(start, end, 12); +``` + +When slot `6` changes in the active palette, every pixel drawn with slot `6` changes automatically. + +--- + +## 3) Index sprites (preferred and manual flows) + +### Preferred: `SpriteSheet.loadIndexed(...)` + +Use this when loading sprites for normal game/demo work. + +```ts +import { SpriteSheet } from 'blit-tech'; + +const indexed = await SpriteSheet.loadIndexed('sprites/hero.png', palette, 32, { sort: 'luminance' }); +BT.paletteSet(palette); +BT.drawSprite(indexed.sheet, indexed.srcRect, pos); +``` + +What it does: + +1. Extracts image colors +2. Registers them into the palette at `startSlot` +3. Loads the sheet +4. Converts source pixels to palette indices + +### Manual: low-level control + +Use this when you need custom slot planning across multiple images. + +```ts +await SpriteSheet.loadColorsIntoPalette('sprites/hero.png', palette, 32); +const sheet = await SpriteSheet.load('sprites/hero.png'); +sheet.indexize(palette); +BT.paletteSet(palette); +``` + +--- + +## 4) Palette offsets (zero-cost color variants) + +`BT.drawSprite(..., paletteOffset)` shifts every stored sprite index before lookup: + +```ts +BT.drawSprite(heroSheet, heroSrc, leftTeamPos, 0); // base range +BT.drawSprite(heroSheet, heroSrc, rightTeamPos, 16); // same art, shifted color range +``` + +Use this for: + +- Team colors +- Seasonal variants +- Damage-state tint sets +- Faction/UI themes + +No duplicate textures required. + +--- + +## 5) Runtime palette effects + +Palette effects mutate slots over time and are applied in the engine frame pipeline. + +```ts +// Cycling: water/lava/plasma +BT.paletteCycle(240, 248, 4); + +// Full-palette fade +BT.paletteFade(nightPalette, 2000, 'ease-in-out'); + +// Range-only fade +BT.paletteFadeRange(32, 63, dangerPalette, 400, 'ease-out'); + +// Temporary flash (slot 0 transparency preserved) +BT.paletteFlash(Color32.white, 120); + +// Instant swap +BT.paletteSwap(10, 11); + +// Cancel running effects +BT.paletteClearEffects(); +``` + +--- + +## 6) Layout swap vs value swap (`BT.spritesRefresh()`) + +This distinction is critical. + +### Value swap (no sprite refresh) + +If slot numbers stay the same and only RGB values change, do **not** refresh sprites. + +Examples: + +- Day/night tinting by editing slot colors +- Palette cycling/fade/flash effects +- Theme tweaks in place + +### Layout swap (refresh required) + +If the same colors move to different slot indices, call: + +```ts +BT.paletteSet(newLayoutPalette); +BT.spritesRefresh(); +``` + +Why: sprite textures store indices. If your palette layout changes, old indices point to wrong colors until re-indexed. + +--- + +## 7) Practical palette-first patterns + +- **Cycling water:** reserve 8 contiguous slots and run `paletteCycle`. +- **Damage flash:** trigger `paletteFlash(Color32.white, durationMs)`. +- **Day/night:** prebuild day/night palettes and use `paletteFade`. +- **HUD stability:** reserve a dedicated slot range and avoid cycling over it. +- **Variant economy:** organize palette in fixed ranges and use sprite offsets instead of duplicate art. + +--- + +## 8) Performance notes + +Palette-first rendering minimizes color-update cost: + +- Color changes are palette writes, not full scene texture rewrites. +- Sprite textures are indexed (one byte per pixel) rather than four-channel RGBA storage. +- Primitive and sprite draws reuse compact index-based pipelines. + +For benchmark workflow and CI thresholds, see [Performance Testing](performance-testing.md). + +--- + +## See Also + +| Guide | What it covers | +| ------------------------------------- | -------------------------------------------------- | +| [API: Palette](api-palette.md) | API reference for palette methods and effect calls | +| [API: Assets](api-assets.md) | `SpriteSheet.loadIndexed`, fonts, and asset flow | +| [API: Rendering](api-rendering.md) | sprite offset semantics and draw APIs | +| [Palette Presets](palette-presets.md) | exact built-in palette color data | +| [Testing](testing.md) | palette testing patterns and visual regression | diff --git a/docs/palette-presets.md b/docs/palette-presets.md new file mode 100644 index 0000000..d3dfc21 --- /dev/null +++ b/docs/palette-presets.md @@ -0,0 +1,245 @@ +# Palette Presets + +Exact built-in color data for `Palette` preset factories and `palette.applyHUD()`. + +All preset hex values are lowercase `RRGGBB` (no `#`), matching `src/assets/palettes/presetData.ts` and +`src/assets/palettes/hudData.ts`. + +--- + + + +## Slot mapping note (important) + +Blit-Tech reserves palette slot `0` for transparency. Preset factories therefore write colors starting at slot `1`. + +That means: + +- the source array's index `0` value is reserved/ignored for rendering +- slot `1` receives source index `1` +- slot `2` receives source index `2` +- etc. + +--- + +## `Palette.vga()` (256 slots) + +VGA preset data is generated in three exact blocks: + +1. **16-color base** +2. **6x6x6 RGB cube** using channel steps `[0, 95, 135, 175, 215, 255]` +3. **24 grayscale values** with level formula `8 + i * 10` for `i = 0..23` + +### VGA base 16 (`VGA_HEX[0..15]`) + +```ts +[ + '000000', + '800000', + '008000', + '808000', + '000080', + '800080', + '008080', + 'c0c0c0', + '808080', + 'ff0000', + '00ff00', + 'ffff00', + '0000ff', + 'ff00ff', + '00ffff', + 'ffffff', +]; +``` + +### VGA cube steps + +```ts +[0, 95, 135, 175, 215, 255]; +``` + +### VGA grayscale ramp (`8..238`, step 10) + +```ts +[8, 18, 28, 38, 48, 58, 68, 78, 88, 98, 108, 118, 128, 138, 148, 158, 168, 178, 188, 198, 208, 218, 228, 238]; +``` + +--- + +## `Palette.cga()` (16 slots) + +```ts +[ + '000000', + '0000aa', + '00aa00', + '00aaaa', + 'aa0000', + 'aa00aa', + 'aa5500', + 'aaaaaa', + '555555', + '5555ff', + '55ff55', + '55ffff', + 'ff5555', + 'ff55ff', + 'ffff55', + 'ffffff', +]; +``` + +--- + +## `Palette.c64()` (16 slots) + +```ts +[ + '000000', + 'ffffff', + '813338', + '75cec8', + '8e3c97', + '56ac4d', + '2e2c9b', + 'edf171', + '8e5029', + '553800', + 'c46c71', + '4a4a4a', + '7b7b7b', + 'a9ff9f', + '706deb', + 'b2b2b2', +]; +``` + +--- + +## `Palette.gameboy()` (4 slots) + +```ts +['0f380f', '306230', '8bac0f', '9bbc0f']; +``` + +--- + +## `Palette.pico8()` (16 slots) + +```ts +[ + '000000', + '1d2b53', + '7e2553', + '008751', + 'ab5236', + '5f574f', + 'c2c3c7', + 'fff1e8', + 'ff004d', + 'ffa300', + 'ffec27', + '00e436', + '29adff', + '83769c', + 'ff77a8', + 'ffccaa', +]; +``` + +--- + +## `Palette.nes()` (64 slots) + +`NES_HEX` currently defines 56 source entries. After transparent slot reservation and preset copy, remaining palette +slots keep their constructor default (black). + +```ts +[ + '7c7c7c', + '0000fc', + '0000bc', + '4428bc', + '940084', + 'a80020', + 'a81000', + '881400', + '503000', + '007800', + '006800', + '005800', + '004058', + '000000', + '000000', + '000000', + 'bcbcbc', + '0078f8', + '0058f8', + '6844fc', + 'd800cc', + 'e40058', + 'f83800', + 'e45c10', + 'ac7c00', + '00b800', + '00a800', + '00a844', + '008888', + '000000', + '000000', + '000000', + 'f8f8f8', + '3cbcfc', + '6888fc', + '9878f8', + 'f878f8', + 'f85898', + 'f87858', + 'fca044', + 'f8b800', + 'b8f818', + '58d854', + '58f898', + '00e8d8', + '787878', + '000000', + '000000', + 'fcfcfc', + 'a4e4fc', + 'b8b8f8', + 'd8b8f8', + 'f8b8f8', + 'f8a4c0', + 'f0d0b0', + 'fce0a8', +]; +``` + +--- + +## HUD preset (`palette.applyHUD`) + +`palette.applyHUD(startSlot = 1)` writes six consecutive slots: + +```ts +[ + { hex: 'ffffff', name: 'hud_white' }, + { hex: '1e1428', name: 'hud_bg' }, + { hex: 'c8c8c8', name: 'hud_label' }, + { hex: 'ffdc64', name: 'hud_header' }, + { hex: '646464', name: 'hud_dim' }, + { hex: '6496c8', name: 'hud_code' }, +]; +``` + +--- + +## See Also + +| Guide | What it covers | +| --------------------------------- | ------------------------------------------------------- | +| [Palette Guide](palette-guide.md) | workflow for setup, offsets, effects, and refresh rules | +| [API: Palette](api-palette.md) | runtime palette APIs and effect signatures | + + diff --git a/docs/software-fallback-smoke-matrix.md b/docs/software-fallback-smoke-matrix.md index d592d82..a622e4e 100644 --- a/docs/software-fallback-smoke-matrix.md +++ b/docs/software-fallback-smoke-matrix.md @@ -22,16 +22,16 @@ in `VV-491` to cover auto-fallback and the dismissible ticker banner. ## Matrix -| Scene | What to verify | Software expected result | Notes | -| --------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------- | ------------------------------------- | -| Any page without `?renderer=software` | Auto-fallback when WebGPU is absent | Demo boots; `BTAPI.getActiveBackend()` = `'software'`; ticker visible | No error page or hard stop | -| Any page with software active | Dismissible ticker banner at top of canvas | Banner centered, dismisses on click/tap; absent next frame | Height 15 px; palette indices 1/2 | -| `tests/visual/fixtures/primitives.html` | clear + clearRect + primitive rasterization | Matches expected primitive layout and colors | Check pixel edges are crisp | -| `tests/visual/fixtures/camera.html` | camera offset applied to all draw calls | Geometry is shifted consistently by camera offset | No partial drift between primitives | -| `tests/visual/fixtures/sprites.html` | indexed sprites + palette offsets + transparency | Sprite shapes/colors match expected output | Transparent pixels stay see-through | -| `tests/visual/fixtures/fonts.html` | system text + bitmap font rendering | Text positions and glyph colors are correct | No missing glyph blocks | -| `tests/visual/fixtures/mixed.html` | primitives + sprites + layering order | Same stacking as WebGPU for this fixture | Parity covered by Playwright snapshot | -| Frame capture via `BT.captureFrame()` | PNG export in software mode | Promise resolves with PNG blob | Repeat capture across multiple frames | +| Scene | What to verify | Software expected result | Notes | +| --------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------- | +| Any page without `?renderer=software` | Auto-fallback when WebGPU is absent | Demo boots; `BT.getActiveBackend()` = `'software'`; ticker visible | No error page or hard stop | +| Any page with software active | Dismissible ticker banner at top of canvas | Banner centered, dismisses on click/tap; absent next frame | Height 15 px; palette indices 1/2 | +| `tests/visual/fixtures/primitives.html` | clear + clearRect + primitive rasterization | Matches expected primitive layout and colors | Check pixel edges are crisp | +| `tests/visual/fixtures/camera.html` | camera offset applied to all draw calls | Geometry is shifted consistently by camera offset | No partial drift between primitives | +| `tests/visual/fixtures/sprites.html` | indexed sprites + palette offsets + transparency | Sprite shapes/colors match expected output | Transparent pixels stay see-through | +| `tests/visual/fixtures/fonts.html` | system text + bitmap font rendering | Text positions and glyph colors are correct | No missing glyph blocks | +| `tests/visual/fixtures/mixed.html` | primitives + sprites + layering order | Same stacking as WebGPU for this fixture | Parity covered by Playwright snapshot | +| Frame capture via `BT.captureFrame()` | PNG export in software mode | Promise resolves with PNG blob | Repeat capture across multiple frames | ## Automated regression diff --git a/docs/testing.md b/docs/testing.md index 86f0765..4140ef8 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,6 +1,7 @@ # Testing -Blit-Tech uses a three-tier testing strategy to cover both pure logic and GPU-dependent rendering code. +Blit-Tech uses three primary testing tiers (unit, integration, visual) plus a benchmark tier for CPU performance +regression tracking. ## Architecture @@ -52,6 +53,11 @@ baseline screenshots for each. - **Fonts** - placeholder text rendering at known positions - **Mixed** - primitives and sprites combined with correct layering +### Tier 4: CPU Benchmarks (Vitest bench) + +Hot-path performance checks for methods and allocation patterns. Benchmarks are tracked in CI benchmark jobs and +documented in [Performance Testing](performance-testing.md). + ## Declaration tooling checks Public types are rolled up during `pnpm build` via `vite-plugin-dts` and API Extractor. The workspace pins TypeScript to @@ -75,6 +81,8 @@ pnpm test:declarations # Declaration tooling log checker (Node test) pnpm test:visual # Playwright visual regression (requires Chrome) pnpm test:visual:update # Update visual test baselines pnpm test:visual:coverage # Visual tests with Istanbul coverage report +pnpm bench # Run CPU benchmarks (Vitest bench) +pnpm bench:json # Run CPU benchmarks and write benchmark-results.json ``` ## Test File Location @@ -125,6 +133,22 @@ Conventions: - No emoji in test descriptions - No JSDoc required in test files +## Palette Testing Patterns + +Use these patterns when adding or reviewing palette-related features: + +- **Slot-zero invariants**: assert slot `0` remains transparent and draw paths treat it as discarded/clear. +- **Preset correctness**: verify preset factories (`Palette.vga`, `cga`, `c64`, `gameboy`, `pico8`, `nes`) and + `palette.applyHUD()` produce expected hex values at known slots. +- **Effect determinism**: for `paletteCycle`, `paletteFade`, `paletteFadeRange`, `paletteFlash`, and `paletteSwap`, + assert both mid-effect and completion states under fixed timestep progression. +- **Dirty flag contract**: assert `set()` / `copyFrom()` / effect updates mark palettes dirty and renderer upload paths + clear the flag after upload. +- **Offset rendering behavior**: add visual assertions that `BT.drawSprite(..., paletteOffset)` remaps the same indexed + sprite data to different slot ranges without duplicating textures. +- **Layout-swap safety**: after swapping to a different index layout, assert `BT.spritesRefresh()` re-indexes tracked + sheets and catches missing-color cases. + ## WebGPU Mock Usage For tests that need GPU objects, import from the mock factory: @@ -220,3 +244,14 @@ Use the Tasks system. Create `.zed/tasks.json`: - **ci.yml**: Unit tests with coverage run on every push/PR to main - **pr-checks.yml**: Unit tests run as part of PR quality checks - **ci.yml (visual job)**: Visual regression runs only on PRs, non-blocking + +--- + +## See Also + +| Guide | What it covers | +| ------------------------------------- | ------------------------------------------ | +| [API: Palette](api-palette.md) | palette APIs and effect signatures | +| [Palette Guide](palette-guide.md) | palette-first workflow and refresh rules | +| [Palette Presets](palette-presets.md) | exact built-in preset and HUD color values | +| [API: Assets](api-assets.md) | indexed sprite setup and palette offsets | diff --git a/package.json b/package.json index d6c77f1..cf80586 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "blit-tech", "version": "1.0.0", "type": "module", - "description": "A lightweight WebGPU retro engine for TypeScript, inspired by RetroBlit.", + "description": "A palette-first WebGPU retro engine for TypeScript, inspired by RetroBlit.", "author": "Václav Vančura", "license": "ISC", "repository": {