BLIT386BLIT386
DocumentationReference

Changelog

Release history in Keep a Changelog style: what shipped, what broke, and when.

All notable changes to blit386 are documented here, in Keep a Changelog style, reverse-chronological, newest first. Categories: Added, Changed, Deprecated, Removed, Fixed, Security. Breaking changes are called out explicitly within their category.

This page is editorial - release highlights and migration notes, not an exhaustive diff. For per-symbol version history (exactly when a given class, method, or getter shipped) see the availability table and changelog on each api/* and guides/* reference page, or GitHub Releases for the full PR-by-PR notes, including dependency bumps and CI changes omitted here for brevity.

1.8.0 - 2026-10-18

Added

  • BT.isReady: a public readiness flag that becomes true only once init() has resolved and the game loop exists, so an external driver (screenshot tool, play-testing harness) can wait before seeking or reading state instead of matching an error message. See Engine readiness.
  • Deterministic seek: BT.renderAt(seconds, { from }) stops the loop and renders exactly the frame at that tick with no requestAnimationFrame, so it works in hidden and headless pages. from: 'start' (default) resets engine-owned state and replays into a fresh instance of the game class (init() runs on a new object, so counters, clocks, and arrays from the replaced run cannot leak in; an object-literal game re-runs init() on itself); from: 'current' steps forward. If the fresh instance's constructor or init() fails, the call rejects, palette effects are cleared, and the previous instance stays active. BT.resume() restarts the loop from the seeked tick, and BT.captureFrame() after a seek captures the seeked frame. The audio context is suspended while a seek holds the loop stopped, and a gesture that unlocks audio mid-seek stays suspended until BT.resume(). See Seeking a frame with renderAt.
  • Nine-slice panels: NineSlice.fromSheet(sheet, outer, inner, { edges, center }) and BT.drawNineSlice(nineSlice, destRect, paletteOffset?) draw one piece of panel art into boxes of any size. Corners stay 1:1; edges and center each stretch (with the sprite stretch rule) or tile (1:1, last tile cropped). Boxes smaller than the corners crop them, so panels can animate open from size 0. Both backends match pixel for pixel. See Nine-slice panels.
  • Sprite pivot: SpriteDrawParams.pivot places a sprite by any point in source pixels instead of its top-left. The point goes through the flags, then the scale, and lands on the Vector2i destination, so a character drawn by its feet stays put when it flips, turns, or grows. An explicit (0, 0) is the source origin and moves under flips, unlike no pivot; assign params.pivot = undefined to reset a reused object. A Rect2i destination with a pivot shows an error. See Flipping, rotating, and scaling sprites.
  • Sprite scale and stretch: SpriteDrawParams.scale takes a positive integer or a Vector2i of positive integers and multiplies the post-flags footprint in screen axes, and the params form of BT.drawSprite accepts a Rect2i destination that the sprite is stretched into. Both use nearest-neighbor sampling at pixel centers, identical on WebGPU and software. A Rect2i needs the params form (BT.drawSprite(sheet, src, rect, {})); development builds show an error when one reaches the fast path. See Flipping, rotating, and scaling sprites.
  • Sprite flip and quarter turns: BT.drawSprite(sheet, src, dest, params) takes a SpriteDrawParams object as the 4th argument, with flags (any combination of BT.FLIP_H, BT.FLIP_V, BT.ROT_90_CW, BT.ROT_180_CW, BT.ROT_270_CW) and paletteOffset. The flags compose in a fixed order into 8 orientations, dest is the top-left of the turned footprint, and both backends match pixel for pixel. The number form of the 4th argument is unchanged and still allocation-free. See Flipping, rotating, and scaling sprites.
  • BT.drawTile: draw one cell of a grid-shaped sprite sheet by row-major index (BT.drawTile(sheet, index, destPos)) or by column and row (BT.drawTile(sheet, col, row, destPos)), with an optional paletteOffset, using a tile size stored on the sheet. BT.drawTile(sheet, col, row, tileSize, destPos) passes the size per call instead. Square or non-square tiles; a partial edge cell is not a tile, and out-of-grid tiles are an error. Allocation-free. Set the grid with the new tileSize option of SpriteSheet.load / SpriteSheet.loadIndexed or the sheet.tileSize property; sheet.tileRect(...) and Rect2i.fromTile(...) return the rectangle. See Drawing tiles.
  • BT.BTN_L2, BT.BTN_R2 and BT.BTN_TRIGGER: digital trigger buttons for BT.isDown / BT.isPressed / BT.isReleased, on at 50% pull or more (fixed threshold). Gamepad only - the keyboard never triggers them and BT.inputMap ignores them; BTN_L / BTN_R remain the shoulders, and BT.isAnyButton* still covers only BTN_UP to BTN_SELECT. See Triggers: analog or digital.
  • BT.isAnyKeyDown(), BT.isAnyKeyPressed(repeatRate?), BT.isAnyKeyReleased(), and the gamepad twins BT.isAnyButtonDown(player?), BT.isAnyButtonPressed(player?, repeatRate?), BT.isAnyButtonReleased(player?): ask about the whole keyboard or one player's gamepad instead of a named key, for "press any key to start" screens. Pressed fires on every new key or button, even with another held. Released follows RetroBlit and means everything is up now, so letting go of one key while another stays down does not fire it. isAnyButton* covers BTN_UP to BTN_SELECT only (no sticks, triggers, keyboard, or pointers). See Any key and any button.
  • BT.nativeScreenToDisplayPos(clientX, clientY): converts viewport coordinates, such as a DOM event's clientX / clientY, to display coordinates with the same math pointer input uses (floor, then clamp to [0, displaySize - 1]). Use it for DOM events the engine does not wrap, such as a file dropped onto the canvas. It returns null before initialization, for a zero-size canvas, or for a non-finite coordinate. Pointer input now shares the same conversion function, so the two cannot drift. See Input guide.
  • keyboardLayout in configure(): 'single' gives player 0 WASD and the arrow keys, so a one-player game reading BT.isDown(BT.BTN_LEFT) finally answers to the left arrow; player 1 moves to IJKL, and keyboard scroll capture turns on unless isCapturingKeyboardScroll is set explicitly. The default 'versus' keeps today's split (WASD for player 0, arrows for player 1), so existing games are unchanged. BT.inputMapReset() restores the active layout's defaults, and the new tables are exposed as BT.DEFAULT_KEYBOARD_SINGLE_PLAYER1 / BT.DEFAULT_KEYBOARD_SINGLE_PLAYER2. See Keyboard layouts.
  • testState() game hook and BT.testState() readout: a game can return plain JSON from an optional testState() method on its class, and an agent play-testing in dev reads it with window.BT.testState() instead of a hand-written window.__game global. The result is { ticks, backend, state }, with an error field when the readout threw. See Test state for agent play-testing.

Changed

  • BT.init: a call made while another init() is still running now resolves to false without touching engine state, and BT.isReady stays false for the whole of any init(), including a re-init after an earlier success. A second bootstrap() during init is refused with a logged error instead of racing the first. See Engine readiness.
  • BT.captureFrame() called while BT.renderAt holds the loop stopped re-renders the seeked frame instead of waiting for a loop frame that never comes. See Frame capture.
  • Palette effects (paletteCycle, fades, flashes) now advance once per fixed update, right after the game's update(), on the tick clock (ticks * 1000 / targetFPS) instead of once per rendered frame on performance.now(). They follow the fixed-step loop rather than wall time or the display's refresh rate, and BT.ticksReset() pauses them for an update instead of running them backwards. A palette write made in render() now lands after the effects rather than before them. An effect's time counts from when it was added, so the first update after an idle gap sees only the time since add, including at tick 0. See Palette effects.
  • The deltaMs passed to post-process effects is tick-clock time since the previous rendered frame, not performance.now(). No built-in effect reads it.
  • @blit386/kit and create-blit386 ship in lockstep as 1.8.0 with no engine API change of their own. Generated games gain agent adapters for Zed, Antigravity, Gemini CLI, OpenCode, Codex, and GitHub Copilot on top of Claude Code and Cursor, all sharing one skills folder and one shell-safety guard core (the Zed paste-in snippet also denies checkout --, restore, and clean); Cursor loads the kit skills on its own; a .claude/launch.json for the Claude desktop preview; the kit hooks format one file per edit and refuse agent edits to any .lock file; the Biome schema version is no longer pinned in scaffolded games; generated Claude settings pre-approve the blit386-docs MCP server, and blit doctor warns when a set-up assistant has no docs config; scaffolded games and the repo tooling work on Windows. The blit386.dev docs MCP server adds a get_doc_page tool with absolute URLs, publishes a build-a-game section, and ranks search by distinct matched terms (two-letter terms such as ui and 2d now match). See GitHub Releases for the full notes.

Fixed

  • A pointer press is no longer lost on a display faster than targetFPS. A click that landed in a render frame with no fixed update used to roll over before update() could read it, so BT.isPressed(BT.BTN_POINTER_A) and the other pointer buttons missed it. The press now carries to the next frame that runs update(). See Frame-timing semantics.
  • A pointer event with an empty pointerType (synthetic events from test harnesses and agents usually omit it) is now routed to the mouse slot instead of falling into the touch branch and being dropped. Synthetic touch or pen events must set pointerType explicitly. See Test state for agent play-testing.

1.7.1 - 2026-09-27

Added

  • Seeding from the URL: opening a page with ?seed=N seeds BT.random automatically, applied right before the demo's own init() runs so a BT.randomSeed() call there still wins. The parameter is read in release builds too, since a seed in a shared bug-report URL is the point; only safe integers are accepted, anything else keeps the time seed. See Random and the random guide.
  • BT.captureFrame() and BT.downloadFrame() accept a new { size: 'display' } option to capture the exact logical BT.displaySize frame instead of the BT.outputSize-matched default. See Frame capture.

Fixed

  • BT.cameraReset() now clears the cached camera offset so a reset persists into the next render frame; previously the engine silently re-applied the stale pre-reset offset on the following frame. See Camera.
  • Touch devices now unlock audio: the unlock listener covered touchstart, which never counts as a user activation for touch input, and a stuck in-flight guard dropped every gesture after the first failed attempt. Audio now unlocks on pointerdown, pointerup, touchend, or keydown, retrying on each gesture. See Locked vs. unlocked.
  • The software backend now rejects display-size and shortcut captures still pending when it re-initializes, matching the WebGPU backend, instead of settling them from the rebuilt scene.

1.7.0 - 2026-09-11

Added

  • Reduced motion: BT.isReducedMotionPreferred reads the browser's prefers-reduced-motion setting, and the optional IBTDemo.onReducedMotionChange(prefersReduced) hook fires when it changes mid-session - the preference can flip while a game is running, the same way orientation can. ?reducedmotion and ?noreducedmotion are valueless URL flags for testing either state without changing OS settings or emulating a media query in devtools. Cutting your own animation fidelity, screen shake, or particle counts stays a game concern; the engine supplies the getter and the hook. The built-in splash already honors it, holding a static frame instead of animating. See Core and the splash guide.
  • Palette#fillBlock(start, source, transform): writes transform(source[i], i) into slot start + i for every source color and returns the next free slot, so consecutive blocks chain. Each write delegates to set(), inheriting its validation including the rule that slot 0 stays transparent. It collapses the palette.set(start + i, transform(baseColors[i])) loop that transformed sprite color blocks kept spelling out by hand. See Palette.
  • window.BT: once bootstrap() finishes, the BT namespace is assigned to window.BT, so the browser console can drive a running game (window.BT.captureFrame()). It follows BT.isDevMode by default, with BootstrapOptions.exposeGlobal overriding in either direction, and the assignment goes through the engine's existing globalThis guard so an SSR or Node import stays inert. See Core.
  • BT.pointerPosTo(out, slot?) and BT.pointerDeltaTo(out, slot?): allocation-free counterparts to pointerPos() / pointerDelta(), which mint a fresh Vector2i on every call. They write into a scratch vector you allocate once and return it, so a demo reading pointer state more than once per frame stops churning garbage. Both write (0, 0) under the same conditions the allocating pair returns Vector2i.zero(), and the slot argument now trails the out param. See Input guide.
  • BT.systemFont: a live reference to the same BitmapFont instance BT.systemPrint draws with, so it can be handed to BT.printFont or queried directly. Alongside it, BitmapFont#codePoints returns every Unicode code point a font defines a glyph for, ascending, derived live from the same glyph map getGlyph() and hasGlyph() read - a font's coverage is now enumerable instead of something to guess at. The system font's own 174-glyph coverage is documented in full. See Rendering and Bitmap Fonts guide.
  • Dev-mode frame capture shortcuts: F9 copies the current frame to the OS clipboard and Shift+F9 downloads it as a timestamped PNG. Both save at logical BT.displaySize rather than BT.outputSize, resolved from the palette-indexed scene buffer, so every logical pixel stays a single output pixel - which also means both bypass display-tier post-process effects such as scanlines, vignette, and bloom. HardwareSettings.isFrameCaptureShortcutEnabled governs them: unset means enabled in development and disabled in release, true keeps them in a production build, false frees both keys. The public BT.captureFrame() / BT.downloadFrame() are untouched and still match BT.outputSize. See Core.

Changed

  • The engine hot-path pass: per-frame and per-call allocations are gone from SoftwareRenderer (per-pixel and per-blit), sprite batching (per-quad UV objects), the BT draw-call wrappers (per-call closures), gamepad polling (now once per frame, reusing its axis arrays), pointer handling (cached canvas bounding rect), the overlay draw path, and GameLoop's rAF callback; the palette also skips its GPU re-upload on ticks that mutate nothing. No public API changed, so existing games collect this by upgrading and nothing else. BT.pointerPosTo / BT.pointerDeltaTo above are the only part of the pass with new surface.

1.6.0 - 2026-09-01

No changes to blit386's public API this release. The version bump is lockstep with @blit386/kit and create-blit386, which shipped a blit386.dev docs MCP server into every scaffolded game and this repository's own root, replaced the website's unsafe-inline script policy with a per-request CSP nonce, and closed a symlink-safety gap in the kit's file operations. See the release blog post or GitHub Releases for the full picture.

1.5.0 - 2026-08-11

Added

  • Seeded random: the Random class (a mulberry32 PRNG) plus BT.random and BT.randomSeed(seed) on the facade. BT.random is a live reference to one shared instance, time-seeded at singleton creation, so the engine default is deterministic the moment you seed it. Beyond next() / float() / int() / intInclusive() it carries the draws games actually reach for - bool, sign, pick, shuffle / shuffleInPlace, weighted, angle, gaussian - and spatial helpers insideRect, pointInRange, direction4, direction8, each with an allocation-free *To(out) variant. Stream control is seed, seedValue, getState, setState, clone, and fork. Prefer it over Math.random(), which cannot be seeded. See Random and Random guide.
  • Coordinate hashing: hash1i / hash2i / hash3i return an unsigned 32-bit value and hash1 / hash2 / hash3 return the same value scaled into [0, 1). These are stateless spatial lookups - identical coordinates and seed always give an identical result - so a chunked or procedural world needs no stored generator per chunk.
  • Pattern noise: ValueNoise, PerlinNoise, and SimplexNoise, each seedable and sampling to approximately [-1, 1]. Value and Perlin expose noise1D / noise2D / noise3D and the matching fbm*; Simplex is 2D and 3D only. fBm defaults to octaves = 4, persistence = 0.5, lacunarity = 2, with octave amplitudes normalized so the sum stays in range. Distinct from the post-process Noise display effect, which is unrelated GPU grain.
  • Full easing curve library on EasingFunction / applyEasing: sine, cubic, quartic, quintic, expo, circ, back, elastic, and bounce families (in / out / in-out), alongside the existing linear and quadratic (ease-*) curves. Math matches RetroBlit's Ease class.
  • interpolate(easing, start, end, t) for number, Vector2i, Color32, and Rect2i. Integer types round to the nearest component; Color32 channels also clamp to [0, 255].
  • BT.paletteFadeExposure(target, durationMs, options?): a palette fade shaped like an in-camera iris pull rather than a post-production crossfade. It interpolates each RGB channel in linear light instead of in encoded values (which is a straight scaling of light when one end of the fade is black), and offsets each entry's schedule by its luminance, so bright entries rise first and hold longest while dark entries arrive late and crush early - toe and shoulder behavior from one knob, highlightLead (0 is a plain linear-light fade, default 0.5) on the exported ExposureFadeOptions bag. Every entry still lands exactly on the target, so nothing drifts. Note the effect is per palette index, not per pixel. See Palette and Palette guide.
  • Color32#toLinear / #toSrgb, plus #toLinearInPlace / #toSrgbInPlace, converting between encoded sRGB and linear light with the real sRGB piecewise transfer function (straight toe below the 0.04045 breakpoint), not a bare 2.2 power. Channels stay 8-bit, so treat linear light as a working space, not a storage format.
  • BT.isDevMode: engine-level dev vs. release build detection. Resolves from the blit386/vite plugin's runtime marker, falling back to a live Vite HMR context, otherwise release. The plugin now sets that marker as a second responsibility beyond hot reload. See Core and Hot Reload.
  • The BLIT386 splash: a logo bitmap fading in on its own 16-step gray ramp, holding, and fading out before the game's first frame. Shown in release builds by default and disabled in development by default, with HardwareSettings.isSplashEnabled and the ?splash / ?nosplash URL flags to override either default. It doubles as a loading screen - the game's init() runs concurrently and the hold extends until it settles - and hands off into the game's palette with a single continuous BT.paletteFadeExposure, so there is no cut. Any key, click, or tap skips, and that press is swallowed. New getters BT.splashState (a SplashState) and BT.isSplashVisible, plus splashColorDark / splashColorLight for the ramp endpoints. The pixelated dissolve is WebGPU-only; the software backend gets the plain fade. See the splash guide and Core.
  • BitmapFont fallback-glyph support: a .btfont file (or the built-in system font) can include a glyph keyed by U+FFFD (the Unicode replacement character), and it is substituted for any character missing its own glyph instead of the character silently vanishing. See Bitmap Fonts guide.
  • The built-in system font (BT.systemPrint) gained 79 extra glyphs beyond plain ASCII: the U+FFFD fallback glyph above, an en dash and em dash, inverted Spanish punctuation (¿, ¡), media/UI icons (play, stop, pause, record, enter, checkmark, bullets and circles, a heart), the four cardinal and four diagonal arrows, uppercase Greek letters, three music notes, a few symbols (×, …, °), and 14 reserved Private Use Area placeholders held open for future glyphs. See scripts/system-font-extra-chars.mjs in the engine source for the full list and each glyph's atlas position.

Changed

  • BitmapFont.getGlyph() / getGlyphByCode() (and measureText(), which now shares their lookup) substitute the font's fallback glyph for a missing character instead of returning null and silently skipping it, when the font defines one. hasGlyph() is unchanged - it still reports true glyph presence, not fallback coverage.

Fixed

  • The engine overlay's top-left title label (BT.systemPrint-rendered, top bar) no longer corrupts demo titles built with an en dash ("BLIT386 Demo - Hypercube", this project's current title format). The label-shortening regex only matched the legacy ASCII-hyphen numbered format, so every current title fell through without shortening into the system font, which lacked a glyph for the en dash - the character vanished and the following characters overdrew the ones before them.

1.4.0 - 2026-07-23

Added

  • Engine-side hot-reload runtime: registerHotReload (wired automatically by the blit386/vite plugin), an optional IBTDemo.onHotReload(context) hook, and a tiered hot-swap - a method-only prototype swap when only method bodies changed, a full re-init when init() or the constructor changed, and a full page reload when hardware settings changed. Demo/game state persists across method and re-init swaps: ticks, camera, palette, and palette effects are never reset. This lands the engine half of BLIT386's HMR support; adoption in blit386-demos/create-blit386 ships in follow-up releases.
  • blit386/vite dev-server plugin: import { blit386 } from 'blit386/vite' in vite.config.ts appends the hot-reload registration snippet to a demo/game's entry module (skipped in production builds) and broadcasts asset changes under configured asset directories (default public/) as blit386:asset-changed HMR events, falling back to a full page reload for an unrecognized asset extension. Ships as a second build entry (dist/vite.js/.cjs/.d.ts) with zero runtime dependency on vite itself.
  • Engine-side asset hot-replace: AssetLoader.evict, plus internal routing in HotRuntime.handleAssetChanged for blit386:asset-changed events. A changed image, audio, or .btfont file under public/ updates the running demo in place - sprite sheets swap their texture (calling indexize() against the active palette when needed), audio clips swap their decoded buffer (stopping SFX voices on the old buffer and restarting the music player if the replaced clip is the current track), and bitmap fonts rebuild their glyph tables and texture - all without a page reload. Adoption in blit386-demos/create-blit386 ships in a follow-up release.
  • Asset loading progress: BT.loadingAssetsCount is polled once per frame as a combined image+audio signal (the sum of AssetLoader.loadingCount and AudioClip.loadingCount) to drive a loading spinner or progress bar until it returns to 0. SpriteSheet.status ('loading' | 'ready' | 'failed') and SpriteSheet.progress expose per-sheet hot-reload replacement state; progress is coarse-grained (0 or 1.0, not a percentage).

Fixed

  • Calling bootstrap() a second time while already initialized no longer silently starts a second, unstoppable GameLoop. With the blit386/vite plugin installed and a hot-reload context registered, a second call now routes to a hot swap instead; without one, it logs an error and returns false.
  • Running under ?backend=software no longer forces a full page reload on every hot-swappable edit. The Tier 3 hardware-settings check now accounts for the URL override on both sides of the comparison, so only a genuine configure() change triggers a reload, matching the behavior already seen under the default webgpu backend.
  • The blit386/vite plugin now syntax-checks a plain .js/.mjs entry module before injecting the hot-reload snippet. Vite's own default transform pipeline excludes those extensions from server-side parsing, so a broken entry module previously surfaced only as a silently caught client-side error - Vite's error overlay never appeared. .ts/.mts entries are unaffected; they already get real syntax validation from Vite's own transform.
  • canvas.style.touch-action was hardcoded to none on attach, blocking touch tap-hold-scroll past the canvas even when HardwareSettings.isCapturingPointerScroll was left at its default false. It is now gated by the same flag: none while pointer scroll capture is active (configure-time opt-in or the overlay's palette-band force), pan-y otherwise, updated live as either source of capture changes.

1.3.1 - 2026-07-19

Added

  • Opt-in keyboard scroll capture via HardwareSettings.isCapturingKeyboardScroll (default false). When enabled, canvas keydown for arrow keys, Space, PageUp/PageDown, Home, and End calls preventDefault() so games can map those keys without scrolling the host page.
  • Opt-in screen wake lock via HardwareSettings.isWakeLockEnabled (default false). When enabled, the engine requests a screen wake lock after a successful init() to prevent mobile browsers from dimming or locking the screen during active gameplay, and re-acquires it automatically once the page returns to the foreground. Silently no-ops on browsers without the Wake Lock API.
  • Screen orientation detection and optional lock: BT.screenOrientation reads the current screen.orientation.type (or null when unavailable); optional IBTDemo.onOrientationChange(type) fires on rotate; HardwareSettings.preferredOrientation ('landscape' | 'portrait' | 'any', default 'any') attempts screen.orientation.lock() after init. Lock failures are silent no-ops (for example on iOS Safari).
  • HardwareSettings.isOverlayToggleHitDebugVisible (default false) draws a 1 pixel outline of the overlay's bottom-left 17×13 toggle hit region, for tuning the hit zone.

Changed

  • Pointer wheel capture is opt-in via HardwareSettings.isCapturingPointerScroll (default false). Canvas wheel events no longer call preventDefault() by default, so host pages can scroll while the pointer is over the canvas. Demos that map BT.pointerScrollDelta must set the flag. The overlay palette grid still forces capture while the pointer is over its band.
  • The overlay's bottom-left toggle hit region shrinks from 48×48 to 17×13 pixels, matching the visible toggle icon more closely.

1.3.0 - 2026-07-13

The audio release: the engine gains a full sound subsystem, from bus mixing to procedural synthesis, plus render-time interpolation for smoother motion between fixed update steps.

Added

  • Audio subsystem with a three-bus mixer (sfx and music feed main, which feeds the destination), per-bus volume, mute, and fades, and honest browser autoplay-gesture unlock tracking: BT.audioVolumeSet / BT.audioVolumeGet, BT.audioMuteSet / BT.isAudioMuted, and the BT.isAudioUnlocked getter.
  • AudioClip asset: streamed fetch and decode with byte-accurate progress, resolved-URL caching with concurrent-request deduplication, and ordered fallback URL lists for browsers that cannot decode the first choice.
  • Sound-effect playback through a fixed-size voice pool with priority-based voice stealing: BT.soundPlay, BT.soundStop, BT.isSoundPlaying, per-voice BT.soundVolumeSet / BT.soundPitchSet / BT.soundPanSet (plus getters), and generational SoundRef handles. HardwareSettings.audioVoices (default 16, range 1-64) sizes the pool.
  • Deterministic procedural synthesis via AudioClip.synth(params) - waveforms, an ADSR envelope, a linear pitch sweep, vibrato, and noise mixing, rendered on the CPU with no source file - plus a preset library BT.synthPreset (jump, pickup, explosion, laser, hit, blip).
  • Crossfading music playback with configurable fade, overlap, and loop points: BT.musicPlay, BT.musicStop, BT.musicVolumeSet / BT.musicVolumeGet, and the BT.isMusicPlaying getter.
  • Overlay audio meters: a per-bus level band with a voices used/total, steal, and drop readout, gated by HardwareSettings.isOverlayAudioMetersEnabled with lazy opt-in metering - no metering cost is paid unless the flag is set.
  • BT.renderAlpha getter: the fractional interpolation factor in [0, 1) between fixed update() steps, for smoothing render state on high-refresh displays. See the new game-loop smoothing guide.

Changed

  • Overlay engine-label separators now render as gap-colored dividers, and numeric overlay fields are padded to a fixed width so the dividers no longer drift frame to frame.
  • Published documentation moved to a site-first workflow (blit386.dev): Fumadocs components, Twoslash-checked code blocks, self-maintaining per-page version badges, and a generated per-symbol API history.

Fixed

  • The camera offset now persists across render frames that run zero update() steps, so scrolling no longer snaps back on high-refresh displays.
  • WebGPU frame capture requests COPY_SRC canvas usage, fixing BT.captureFrame() / BT.downloadFrame() on backends that require it.
  • BT.systemPrintMeasure() returns a fresh size object on every call instead of mutating a shared instance.
  • The engine overlay's toggle key press is now sampled during the fixed-update tick instead of the render phase, fixing intermittently dropped toggle presses under rapid input on high-refresh displays.

1.2.1 - 2026-06-20

Fixed

  • Breaking: KeyboardInput now buffers press and release edges into pendingPress/pendingRelease queues, consumed by endUpdate() at the fixed update boundary. On 120+ Hz displays with a 60 FPS target, fast taps between fixed update steps were previously dropped silently. endFrame is deprecated and forwards to endUpdate.
  • Stray keyup events for keys that were never held down no longer fire a released edge.

Changed

  • "See also" cross-reference sections added or expanded across 16 documentation files; README.md rewritten with a beginner-first voice.

1.2.0 - 2026-06-19

Package and API rename - the engine is now published as blit386 (previously blit-tech).

Changed

  • Breaking: npm package renamed from blit-tech to blit386. Update import paths and package.json dependencies.
  • Breaking: Public demo interface renamed from IBlitTechDemo to IBTDemo.
  • Breaking: Default canvas ID changed from blit-tech-canvas to blit386-canvas; default frame-capture filename changed to blit386-capture.png.

Fixed

  • GPUTextureUsage was evaluated at module load time via a static import of the WebGPU renderer, crashing non-WebGPU browsers (including Firefox on Linux) before the Canvas 2D fallback could activate. Access is now inlined at call sites.

1.1.1 - 2026-06-07

Fixed

  • WebGPURenderer was imported statically, evaluating GPUTextureUsage at module load time. Firefox and any other browser without WebGPU globals crashed before the Canvas 2D fallback had a chance to run.

Changed

  • Boolean queries and predicate methods normalized to is*/has* conventions: isDown, isPressed, isPointerActive, isEqual, isContaining, isIntersecting. HardwareSettings config flags renamed to isOverlayEnabled, isDetectingDroppedFrames, and so on. Deprecated aliases keep the previous names working - see the Deprecation Timeline.
  • Asset system structural refactor across BitmapFont, SpriteSheet, Palette, PaletteEffect, and SystemFont - internal only, no public API change.
  • Documentation corrected and expanded across 45+ files; a new overlay guide added.

1.1.0 - 2026-05-30

The overlay release: the old software-mode ticker is replaced by a full engine HUD subsystem.

Added

  • New src/overlay/ subsystem: a toggle-able bar stack with a live palette swatch grid.
  • Scrolling timing chart with configurable grid lines (gridPaletteIndex), severity tints for dropped frames and high-latency spikes, and scrolling event tags via BT.assignTag().
  • WebGPU renderer pipeline diagnostics in the timing snapshot; a software-renderer diagnostics bar in the overlay.
  • BT.requestedBackend getter for the resolved init backend, fixed even after a WebGPU-to-software fallback.

Changed

  • Breaking: HardwareSettings.renderer renamed to backend; the URL override is now ?backend=software.
  • Breaking: canvasDisplaySize/maxCanvasDisplaySize replaced by drawingBufferSize/maxCanvasSize.
  • Breaking: every statsOverlay* setting, the statsOverlayRows() hook, and the StatsOverlay* exported types drop the stats/StatsOverlay prefix (for example overlayEnabled, overlayRows(), OverlayRow).

1.0.5 - 2026-05-21

Added

  • MCP preflight infrastructure with documented offline fallbacks; a CI dependency audit on every PR.

Changed

  • configure() now accepts a partial HardwareSettings object - demos can override just targetFPS without spelling out every field. The engine merges the result with defaultConfig() via the new exported mergeHardwareSettings().

Security

  • .btfont embedded textures must now use data:image/png;base64 with a 512 KiB payload cap; glyph metrics are validated before atlas decode begins. A new AssetLimits module centralizes these constants.

1.0.4 - 2026-05-17

BT namespace read-only properties changed from methods to ES getters.

Changed

  • Breaking: call parentheses dropped from every BT read-only accessor - BT.displaySize() becomes BT.displaySize, BT.ticks() becomes BT.ticks, and so on across deltaSeconds, timeSeconds, pointerScrollDelta, gamepadCount, and inputString.
  • Breaking: four accessors renamed - fps() to BT.targetFPS, getActiveBackend() to BT.activeBackend, paletteGet() to BT.palette, cameraGet() to BT.camera.

Added

  • New getters: BT.canvasDisplaySize and BT.outputSize (canvasDisplaySize ?? displaySize).

Security

  • RenderLimits and AssetLimits cap render and asset dimensions before any GPU or buffer allocation. pnpm 10 supply-chain hardening enabled (minimum-release-age, block-exotic-subdeps, strict-dep-builds). CI action tags pinned to commit SHAs.

1.0.3 - 2026-05-16

First stable release, closing out the 0.2.x prototype arc. The rendering stack is palette-first end to end, the input system is complete across pointer, keyboard, and gamepad, and the BT namespace reaches its stable shape.

Added

  • Palette-first rendering pipeline: an r8uint framebuffer stores one palette slot per pixel; a PaletteResolveUpscalePass converts to RGBA immediately before output.
  • Canvas 2D SoftwareRenderer fallback, activating automatically when WebGPU is unavailable: palette rendering, Bresenham lines, indexed sprite blits, bitmap text. Post-process effects throw with a redirect to the WebGPU backend.
  • Complete input system: PointerInput (mouse plus 3 touch/pen slots), KeyboardInput (edge detection, tick repeat, text input buffer), and GamepadInput (dead-zone-filtered axes, per-player connectivity), all sharing the BTN_* bitmask API.
  • Stackable post-process effect chain: pixel-tier effects run on the palette-index buffer, display-tier effects run on the upscaled RGBA output. Pre-configured crtPipBoy, amber, and green presets included.
  • Palette class with 256-color support, serialization, named indices, and a dirty flag that auto-propagates mutations to the GPU.
  • Frame capture to PNG via GPU readback (BT.captureFrame(), BT.downloadFrame()).
  • Built-in 6×14 system font with BT.systemPrint() / BT.systemPrintMeasure() - render text without loading external font files.
  • Timer utility for fixed-timestep interval checks in update loops.

Changed

  • Breaking: IBlitTechGame renamed to IBlitTechDemo; bootstrap() accepts a demo class rather than a game class; default canvas ID changed from game-canvas to blit-tech-canvas.
  • Breaking: BT.initialize() and IBlitTechDemo.initialize() shortened to init() across the entire engine lifecycle.
  • Breaking: IBlitTechDemo.queryHardware() renamed to configure() and made optional; omitting it falls back to defaultConfig() (320×240 display, 640×480 canvas, 60 FPS, nearest-neighbor upscale).
  • Breaking: Color32.white(), black(), transparent(), and the other color-primary statics converted from zero-argument methods to static getters returning frozen singletons - drop the call parentheses.
  • BTAPI split into three focused modules: GameLoop (fixed-timestep accumulator), WebGPUContext (adapter/device/context setup), delegated to by BTAPI itself.

Removed

  • Obsolete WebGPU-only bootstrap helpers: checkWebGPUSupport, detectBrowser, getWebGPUInstructions, buildErrorPreviewEntries, previewWebGPUErrors - superseded by the SoftwareRenderer auto-fallback and a shared errorMessages.ts module.

0.2.0 - 2025-12-22

Pre-release published under the original blit-tech package name.

Added

  • Bootstrap utilities for streamlined game initialization.
  • CI checks: commit message linting, bundle size monitoring, documentation link validation, spell checking with cspell.

0.1.0 - 2025-12-13

Initial pre-release published under the original blit-tech package name.

Added

  • Display size and tick counter APIs on Core.
  • Cache-checking APIs in AssetLoader.
  • Variable-width bitmap font system with the .btfont format.
  • ImageBitmap support and a destroy() method on SpriteSheet.
  • Enhancements to Color32, Rect2i, and Vector2i.

See also

Last updated on October 18, 2026

On this page