diff --git a/CLAUDE.md b/CLAUDE.md index ebb43fa..788c521 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ fantasy-console-style API. ## Architecture All engine functionality is accessed through the static `BT` namespace. Demos implement the `IBlitTechDemo` interface -(`queryHardware`, `init`, `update`, `render`). +(`configure?`, `init`, `update`, `render`). ```text src/ diff --git a/README.md b/README.md index 316aefe..dad7a6e 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ primitives, and fonts. `BT.getAxis`), with stick dead zone and face-button support through `BT.button*` - **Fixed timestep**: deterministic 60 FPS loop with tick counter and optional dropped-frame detection - **Clean API**: all engine access through the `BT` namespace -- **Display scaling**: optional CSS upscaling via `canvasDisplaySize` for crisp pixel art +- **Display scaling**: `canvasDisplaySize` drives the WebGPU drawing buffer and CSS size for crisp pixel art; engine + `defaultConfig()` uses a `640x480` output for `320x240` logical (2x nearest) when a demo omits `configure()` ## Prerequisites @@ -121,7 +122,8 @@ Additional documentation is available in the `docs/` directory: ## Quick Start -Create a demo by implementing the `IBlitTechDemo` interface: +Create a demo by implementing the `IBlitTechDemo` interface. The optional `configure()` hook overrides engine defaults +(`320x240` logical, `640x480` output, `60` FPS from `defaultConfig()`); omit it for minimal demos. ```ts import { @@ -142,12 +144,12 @@ const BLUE = 3; class MyDemo implements IBlitTechDemo { /** - * Configures hardware settings for this demo. - * Sets up a 320×240 internal resolution with optional CSS upscaling. + * Optional: configures hardware settings for this demo. + * Sets up a 320×240 internal resolution with optional output upscaling. * * @returns Hardware configuration specifying display size and target FPS. */ - queryHardware(): HardwareSettings { + configure(): HardwareSettings { return { displaySize: new Vector2i(320, 240), // Internal rendering resolution canvasDisplaySize: new Vector2i(640, 480), // Optional: CSS display size (2× upscale) @@ -382,12 +384,12 @@ organized into two chains by what they operate on: RGB shadow mask, vignette, chromatic aberration, bloom, etc. Operating at output resolution is what lets curved sampling (barrel) express smoothly without quantizing onto the source pixel grid. -Both chains add zero cost when empty. Set `canvasDisplaySize` in `queryHardware()` to enable the display tier. +Both chains add zero cost when empty. Set `canvasDisplaySize` in `configure()` to enable the display tier. ```ts import { BT, Vector2i, BarrelDistortion, Scanlines, Bloom, PixelGlitch } from 'blit-tech'; -// In queryHardware(): unlock the display tier and pick an output size. +// In configure(): unlock the display tier and pick an output size. return { displaySize: new Vector2i(320, 240), canvasDisplaySize: new Vector2i(1280, 960), // 4x integer scale @@ -516,7 +518,7 @@ BitmapFont.load(url); // Load bitmap font (static method) #### Pointer (mouse, touch, pen) The engine tracks up to four pointer slots. Slot 0 is always the mouse; slots 1-3 are touch and pen contacts assigned in -arrival order. All coordinates are in logical display space (the `displaySize` from `queryHardware()`). +arrival order. All coordinates are in logical display space (the `displaySize` from `configure()` or defaults). ```ts // Position and movement diff --git a/docs/input.md b/docs/input.md index 47eabb5..b6c7bbe 100644 --- a/docs/input.md +++ b/docs/input.md @@ -4,8 +4,8 @@ Blit-Tech provides DOM-backed input: **pointer** (mouse, touch, pen), **keyboard virtual face buttons), **gamepad** (up to four players via `navigator.getGamepads()`), and **text accumulation** for UI entry (`BT.inputString()`). -All pointer coordinates are returned in logical display space (the `displaySize` configured in `queryHardware()`), -independent of the canvas's CSS or backing-buffer size. +All pointer coordinates are returned in logical display space (the `displaySize` from `configure()` or +`defaultConfig()`), independent of the canvas's CSS or backing-buffer size. ## Pointer Slot Model diff --git a/docs/performance-best-practices.md b/docs/performance-best-practices.md index 3a2e1c1..b04256c 100644 --- a/docs/performance-best-practices.md +++ b/docs/performance-best-practices.md @@ -238,7 +238,7 @@ if (elapsedTime >= 1.0) { If your `update()` or `render()` takes too long: -1. **Confirm the symptom** - Set `detectDroppedFrames: true` in `queryHardware()` to log a console warning whenever the +1. **Confirm the symptom** - Set `detectDroppedFrames: true` in `configure()` to log a console warning whenever the browser misses a vsync deadline. The detector auto-calibrates to the actual rAF cadence so it works on any refresh rate (60 / 120 / 144 Hz, etc.) and on Firefox where rAF often fires at the display rate rather than at `targetFPS`. 2. **Profile with browser dev tools** - Find the actual bottleneck diff --git a/docs/post-process-effects.md b/docs/post-process-effects.md index 895ce40..1a4cec1 100644 --- a/docs/post-process-effects.md +++ b/docs/post-process-effects.md @@ -22,7 +22,7 @@ your own, the bundled presets, and the upstream attribution. import { BT, Vector2i, BarrelDistortion, Scanlines, RGBMask, Bloom, PixelGlitch } from 'blit-tech'; class Demo { - queryHardware() { + configure() { return { displaySize: new Vector2i(320, 240), // logical pixel-art resolution canvasDisplaySize: new Vector2i(1280, 960), // output drawing-buffer size @@ -141,8 +141,12 @@ interface HardwareSettings { } ``` -When `canvasDisplaySize` is omitted, the WebGPU drawing buffer matches `displaySize`, no upscale pass exists, and the -display tier is unavailable (adding a display effect throws). +When `canvasDisplaySize` is omitted from your **returned** `HardwareSettings`, the WebGPU drawing buffer matches +`displaySize`, no upscale pass exists, and the display tier is unavailable (adding a display effect throws). + +If the demo **does not** implement `configure()`, the engine uses `defaultConfig()`, which **does** set +`canvasDisplaySize` (`640x480` for `320x240` logical), so the display tier remains available unless you override with a +custom `configure()` that omits output sizing. --- diff --git a/docs/testing.md b/docs/testing.md index 871ecc5..087b94d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -15,7 +15,7 @@ Pure logic with no GPU dependencies. Tests run in Node.js for maximum speed. - **PaletteEffect** - manager lifecycle, CycleEffect rotation, FadeEffect/FadeRangeEffect lerp, FlashEffect, paletteSwap - **Easing** - boundary values, monotonicity, midpoint checks for all easing curves - **GameLoop** - constructor validation, tick counter -- **IBlitTechDemo** - default hardware settings +- **IBlitTechDemo** - `defaultConfig()` and optional `configure()` ### Tier 2: Integration Tests (Vitest, Node + GPU mocks) diff --git a/src/BlitTech.test.ts b/src/BlitTech.test.ts index 06b0e7a..f105998 100644 --- a/src/BlitTech.test.ts +++ b/src/BlitTech.test.ts @@ -35,7 +35,7 @@ describe('BT.init', () => { it('delegates to BTAPI.instance.init and returns its result', async () => { const spy = vi.spyOn(BTAPI.instance, 'init').mockResolvedValue(true); const demo = { - queryHardware: vi.fn(), + configure: vi.fn(), init: vi.fn(), update: vi.fn(), render: vi.fn(), @@ -52,7 +52,7 @@ describe('BT.init', () => { vi.spyOn(BTAPI.instance, 'init').mockResolvedValue(false); const demo = { - queryHardware: vi.fn(), + configure: vi.fn(), init: vi.fn(), update: vi.fn(), render: vi.fn(), diff --git a/src/BlitTech.ts b/src/BlitTech.ts index 3cb359b..2e263f9 100644 --- a/src/BlitTech.ts +++ b/src/BlitTech.ts @@ -16,7 +16,7 @@ import { BitmapFont } from './assets/BitmapFont'; import { Palette } from './assets/Palette'; import { SpriteSheet } from './assets/SpriteSheet'; import { BTAPI } from './core/BTAPI'; -import { defaultHardwareSettings, type HardwareSettings, type IBlitTechDemo } from './core/IBlitTechDemo'; +import { defaultConfig, type HardwareSettings, type IBlitTechDemo } from './core/IBlitTechDemo'; import { createDefaultKeyboardRuntimeMaps, DEFAULT_KEYBOARD_PLAYER1, @@ -488,7 +488,8 @@ export const BT = { * * - `tier='pixel'` -> pixel chain (logical resolution). * - `tier='display'` -> display chain (output resolution); requires - * `canvasDisplaySize` to be set in `queryHardware()`. + * `canvasDisplaySize` in effective hardware settings (`configure()` or + * `defaultConfig()`). * * Effects run in registration order within each tier. The pixel chain runs * first, followed by the upscale pass, followed by the display chain. Each @@ -1237,7 +1238,7 @@ export { ChromaticAberration, Color32, crtPipBoy, - defaultHardwareSettings, + defaultConfig, displayError, Flicker, getCanvas, diff --git a/src/core/BTAPI.test.ts b/src/core/BTAPI.test.ts index de8b364..dde00a2 100644 --- a/src/core/BTAPI.test.ts +++ b/src/core/BTAPI.test.ts @@ -39,7 +39,7 @@ function resetSingleton(): void { function makeMockDemo(targetFPS = 60, initResult = true): IBlitTechDemo { return { - queryHardware: vi.fn().mockReturnValue({ + configure: vi.fn().mockReturnValue({ displaySize: new Vector2i(320, 240), canvasDisplaySize: new Vector2i(640, 480), targetFPS, @@ -379,6 +379,28 @@ describe('BTAPI', () => { expect(BTAPI.instance.getGamepad()).not.toBeNull(); }); + it('should use defaultConfig when configure is omitted', async () => { + const demo: IBlitTechDemo = { + init: vi.fn().mockResolvedValue(true), + update: vi.fn(), + render: vi.fn(), + }; + + const result = await BTAPI.instance.init(demo, makeMockCanvas()); + + expect(result).toBe(true); + + const hw = BTAPI.instance.getHardwareSettings(); + + expect(hw).not.toBeNull(); + expect(hw?.displaySize.x).toBe(320); + expect(hw?.displaySize.y).toBe(240); + expect(hw?.canvasDisplaySize?.x).toBe(640); + expect(hw?.canvasDisplaySize?.y).toBe(480); + expect(hw?.outputUpscaleFilter).toBe('nearest'); + expect(hw?.targetFPS).toBe(60); + }); + it('stop detaches pointer and keyboard input so subsequent accessors return null', async () => { await BTAPI.instance.init(makeMockDemo(), makeMockCanvas()); diff --git a/src/core/BTAPI.ts b/src/core/BTAPI.ts index ac24f3d..6724421 100644 --- a/src/core/BTAPI.ts +++ b/src/core/BTAPI.ts @@ -22,6 +22,7 @@ import { Vector2i } from '../utils/Vector2i'; import type { FrameDropCallback, FrameDropEvent } from './GameLoop'; import { GameLoop } from './GameLoop'; import type { HardwareSettings, IBlitTechDemo } from './IBlitTechDemo'; +import { defaultConfig } from './IBlitTechDemo'; import { initWebGPU } from './WebGPUContext'; /** @@ -143,7 +144,7 @@ export class BTAPI { * Initializes the engine for a demo and starts the main loop on success. * * The initialization sequence is: - * - query hardware settings from the demo + * - read hardware settings from the demo (`configure()` or defaults) * - initialize WebGPU and create the renderer * - run the demo's async `init()` * - start the fixed-timestep game loop @@ -158,10 +159,10 @@ export class BTAPI { this.demo = demo; this.canvas = canvas; - // Query hardware settings from the demo. - console.log('[BT] Querying hardware settings'); + // Hardware settings: demo hook or defaults (320x240 @ 60 FPS). + console.log('[BT] Reading hardware configuration'); - this.hwSettings = demo.queryHardware(); + this.hwSettings = demo.configure?.() ?? defaultConfig(); const { targetFPS } = this.hwSettings; @@ -318,7 +319,8 @@ export class BTAPI { // #region Demo State Accessors /** - * Gets the hardware settings returned by the active demo. + * Gets the hardware settings used for the active demo (from `configure()` or + * {@link defaultConfig}). * * @returns Hardware configuration, or null if not initialized. */ diff --git a/src/core/IBlitTechDemo.test.ts b/src/core/IBlitTechDemo.test.ts index 74fc5cc..a31a847 100644 --- a/src/core/IBlitTechDemo.test.ts +++ b/src/core/IBlitTechDemo.test.ts @@ -1,6 +1,5 @@ /** - * Unit tests for the default hardware-settings helper exported by - * {@link IBlitTechDemo}. + * Unit tests for {@link defaultConfig} exported from {@link IBlitTechDemo}. * * Confirms the default display resolution and frame rate, ensures optional * canvas sizing is omitted by default, and verifies each call returns fresh @@ -9,31 +8,38 @@ import { describe, expect, it } from 'vitest'; -import { defaultHardwareSettings } from './IBlitTechDemo'; +import { defaultConfig } from './IBlitTechDemo'; -describe('defaultHardwareSettings', () => { +describe('defaultConfig', () => { it('should return 320x240 display size', () => { - const settings = defaultHardwareSettings(); + const settings = defaultConfig(); expect(settings.displaySize.x).toBe(320); expect(settings.displaySize.y).toBe(240); }); it('should return the 60 FPS target', () => { - const settings = defaultHardwareSettings(); + const settings = defaultConfig(); expect(settings.targetFPS).toBe(60); }); - it('should not include canvasDisplaySize by default', () => { - const settings = defaultHardwareSettings(); + it('should include 640x480 canvasDisplaySize by default', () => { + const settings = defaultConfig(); - expect(settings.canvasDisplaySize).toBeUndefined(); + expect(settings.canvasDisplaySize?.x).toBe(640); + expect(settings.canvasDisplaySize?.y).toBe(480); + }); + + it("should default outputUpscaleFilter to 'nearest'", () => { + const settings = defaultConfig(); + + expect(settings.outputUpscaleFilter).toBe('nearest'); }); it('should return a fresh object on each call', () => { - const a = defaultHardwareSettings(); - const b = defaultHardwareSettings(); + const a = defaultConfig(); + const b = defaultConfig(); expect(a).not.toBe(b); expect(a.displaySize).not.toBe(b.displaySize); diff --git a/src/core/IBlitTechDemo.ts b/src/core/IBlitTechDemo.ts index a8991f5..ed4aebf 100644 --- a/src/core/IBlitTechDemo.ts +++ b/src/core/IBlitTechDemo.ts @@ -9,7 +9,8 @@ import { Vector2i } from '../utils/Vector2i'; export type OutputUpscaleFilter = 'nearest' | 'linear'; /** - * Engine-facing hardware configuration returned by `queryHardware()`. + * Engine-facing hardware configuration returned by `configure()` when a demo + * implements that optional hook, or by {@link defaultConfig} otherwise. */ export interface HardwareSettings { /** Logical render resolution in pixels (e.g. `320x240`). */ @@ -59,19 +60,22 @@ export interface HardwareSettings { * Demo contract implemented by Blit-Tech applications. * * Engine lifecycle order: - * 1. queryHardware() - Called first to configure display/FPS + * 1. configure() - Optional; called first to set display size, output buffer, FPS * 2. init() - Called after WebGPU setup, load assets here * 3. update() - Fixed timestep via accumulator (may run 0..N times per frame) * 4. render() - Called once per requestAnimationFrame (browser refresh rate) */ export interface IBlitTechDemo { /** - * Called first to declare display size, optional output drawing-buffer size, + * Optional hook to declare display size, optional output drawing-buffer size, * upscale filter, and the target fixed-update rate. * + * When omitted, the engine uses {@link defaultConfig} (`320x240` at + * `60` FPS). + * * @returns Hardware configuration for this demo. */ - queryHardware(): HardwareSettings; + configure?(): HardwareSettings; /** * Called once after WebGPU and the renderer have been initialized. @@ -111,16 +115,19 @@ export interface IBlitTechDemo { // #region Helper Functions /** - * Creates a fresh default hardware-settings object for quick demos. + * Creates a fresh default hardware configuration for quick demos. * - * The defaults are a `320x240` internal resolution and a `60` FPS update rate. + * Matches the most common setup across Blit-Tech demos: `320x240` logical resolution, + * `640x480` canvas output (2x nearest upscale), and `60` FPS fixed updates. * * @returns Default HardwareSettings configuration. */ -export function defaultHardwareSettings(): HardwareSettings { +export function defaultConfig(): HardwareSettings { return { displaySize: new Vector2i(320, 240), + canvasDisplaySize: new Vector2i(640, 480), targetFPS: 60, + outputUpscaleFilter: 'nearest', }; } diff --git a/src/render/Renderer.ts b/src/render/Renderer.ts index 050c8dc..338c2e5 100644 --- a/src/render/Renderer.ts +++ b/src/render/Renderer.ts @@ -73,7 +73,7 @@ export class Renderer { /** * True when the caller explicitly provided an `outputSize` (i.e. set - * `canvasDisplaySize` in `queryHardware()`), enabling display-tier effects + * `canvasDisplaySize` in `configure()`), enabling display-tier effects * regardless of whether the output resolution differs from the logical one. */ private readonly displayTierEnabled: boolean; @@ -487,7 +487,7 @@ export class Renderer { * * - `tier='pixel'` -> pixel chain (logical resolution). * - `tier='display'` -> display chain (output resolution); requires - * `canvasDisplaySize` to be set in `queryHardware()`. + * `canvasDisplaySize` to be set in `configure()`. * * @param effect - Effect instance to append. * @throws If the renderer has not been initialized. @@ -501,7 +501,7 @@ export class Renderer { if (effect.tier === 'display' && !this.displayTierEnabled) { throw new Error( - 'Renderer.addEffect: display-tier effects require canvasDisplaySize to be set in queryHardware().', + 'Renderer.addEffect: display-tier effects require canvasDisplaySize to be set in configure().', ); } diff --git a/src/utils/Bootstrap.test.ts b/src/utils/Bootstrap.test.ts index d70784b..04cdf92 100644 --- a/src/utils/Bootstrap.test.ts +++ b/src/utils/Bootstrap.test.ts @@ -21,15 +21,10 @@ import { BTAPI } from '../core/BTAPI'; import type { IBlitTechDemo } from '../core/IBlitTechDemo'; import { bootstrap } from './Bootstrap'; import { DEFAULT_CANVAS_ID, DEFAULT_CONTAINER_ID } from './BootstrapHelpers'; -import { Vector2i } from './Vector2i'; // #region Test Demo class MockDemo implements IBlitTechDemo { - queryHardware() { - return { displaySize: new Vector2i(320, 240), targetFPS: 60 }; - } - async init() { return true; } diff --git a/src/utils/Bootstrap.ts b/src/utils/Bootstrap.ts index 3edd7b6..8b6f553 100644 --- a/src/utils/Bootstrap.ts +++ b/src/utils/Bootstrap.ts @@ -246,7 +246,7 @@ async function initDemo( * This function provides a streamlined way to start a demo with sensible defaults * while allowing customization through options. * - * @param DemoClass - Demo class constructor implementing IBlitTechDemo. + * @param DemoClass - Demo class constructor implementing `IBlitTechDemo` (optional `configure()` for hardware settings). * @param options - Optional configuration for IDs and callbacks. * @returns `true` when the demo boots successfully; otherwise `false`. * diff --git a/tests/visual/fixtures/camera.html b/tests/visual/fixtures/camera.html index 7fe0762..0eabaec 100644 --- a/tests/visual/fixtures/camera.html +++ b/tests/visual/fixtures/camera.html @@ -31,7 +31,7 @@ const WHITE = 8; class CameraTest { - queryHardware() { + configure() { return { displaySize: new Vector2i(320, 240), targetFPS: 60, diff --git a/tests/visual/fixtures/fonts.html b/tests/visual/fixtures/fonts.html index dd68422..df0acf2 100644 --- a/tests/visual/fixtures/fonts.html +++ b/tests/visual/fixtures/fonts.html @@ -30,7 +30,7 @@ const WHITE = 8; class FontsTest { - queryHardware() { + configure() { return { displaySize: new Vector2i(320, 240), targetFPS: 60, diff --git a/tests/visual/fixtures/mixed.html b/tests/visual/fixtures/mixed.html index 34fc834..ec89c69 100644 --- a/tests/visual/fixtures/mixed.html +++ b/tests/visual/fixtures/mixed.html @@ -55,7 +55,7 @@ class MixedTest { spriteSheet = null; - queryHardware() { + configure() { return { displaySize: new Vector2i(320, 240), targetFPS: 60, diff --git a/tests/visual/fixtures/post-process.html b/tests/visual/fixtures/post-process.html index 7b2d4e8..bed2272 100644 --- a/tests/visual/fixtures/post-process.html +++ b/tests/visual/fixtures/post-process.html @@ -67,7 +67,7 @@ const WHITE = 8; class PostProcessTest { - queryHardware() { + configure() { const cfg = { displaySize: new Vector2i(320, 240), targetFPS: 60, diff --git a/tests/visual/fixtures/primitives.html b/tests/visual/fixtures/primitives.html index 02a7f3d..73bcaa6 100644 --- a/tests/visual/fixtures/primitives.html +++ b/tests/visual/fixtures/primitives.html @@ -32,7 +32,7 @@ const WHITE = 8; class PrimitivesTest { - queryHardware() { + configure() { return { displaySize: new Vector2i(320, 240), targetFPS: 60, diff --git a/tests/visual/fixtures/sprites.html b/tests/visual/fixtures/sprites.html index b789212..6f3cbff 100644 --- a/tests/visual/fixtures/sprites.html +++ b/tests/visual/fixtures/sprites.html @@ -65,7 +65,7 @@ class SpritesTest { spriteSheet = null; - queryHardware() { + configure() { return { displaySize: new Vector2i(320, 240), targetFPS: 60,