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 becomestrueonly onceinit()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 norequestAnimationFrame, 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-runsinit()on itself);from: 'current'steps forward. If the fresh instance's constructor orinit()fails, the call rejects, palette effects are cleared, and the previous instance stays active.BT.resume()restarts the loop from the seeked tick, andBT.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 untilBT.resume(). See Seeking a frame with renderAt. - Nine-slice panels:
NineSlice.fromSheet(sheet, outer, inner, { edges, center })andBT.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.pivotplaces 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 theVector2idestination, 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; assignparams.pivot = undefinedto reset a reused object. ARect2idestination with a pivot shows an error. See Flipping, rotating, and scaling sprites. - Sprite scale and stretch:
SpriteDrawParams.scaletakes a positive integer or aVector2iof positive integers and multiplies the post-flags footprint in screen axes, and the params form ofBT.drawSpriteaccepts aRect2idestination that the sprite is stretched into. Both use nearest-neighbor sampling at pixel centers, identical on WebGPU and software. ARect2ineeds 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 aSpriteDrawParamsobject as the 4th argument, withflags(any combination ofBT.FLIP_H,BT.FLIP_V,BT.ROT_90_CW,BT.ROT_180_CW,BT.ROT_270_CW) andpaletteOffset. The flags compose in a fixed order into 8 orientations,destis 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 optionalpaletteOffset, 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 newtileSizeoption ofSpriteSheet.load/SpriteSheet.loadIndexedor thesheet.tileSizeproperty;sheet.tileRect(...)andRect2i.fromTile(...)return the rectangle. See Drawing tiles.BT.BTN_L2,BT.BTN_R2andBT.BTN_TRIGGER: digital trigger buttons forBT.isDown/BT.isPressed/BT.isReleased, on at 50% pull or more (fixed threshold). Gamepad only - the keyboard never triggers them andBT.inputMapignores them;BTN_L/BTN_Rremain the shoulders, andBT.isAnyButton*still covers onlyBTN_UPtoBTN_SELECT. See Triggers: analog or digital.BT.isAnyKeyDown(),BT.isAnyKeyPressed(repeatRate?),BT.isAnyKeyReleased(), and the gamepad twinsBT.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*coversBTN_UPtoBTN_SELECTonly (no sticks, triggers, keyboard, or pointers). See Any key and any button.BT.nativeScreenToDisplayPos(clientX, clientY): converts viewport coordinates, such as a DOM event'sclientX/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 returnsnullbefore 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.keyboardLayoutinconfigure():'single'gives player 0 WASD and the arrow keys, so a one-player game readingBT.isDown(BT.BTN_LEFT)finally answers to the left arrow; player 1 moves to IJKL, and keyboard scroll capture turns on unlessisCapturingKeyboardScrollis 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 asBT.DEFAULT_KEYBOARD_SINGLE_PLAYER1/BT.DEFAULT_KEYBOARD_SINGLE_PLAYER2. See Keyboard layouts.testState()game hook andBT.testState()readout: a game can return plain JSON from an optionaltestState()method on its class, and an agent play-testing in dev reads it withwindow.BT.testState()instead of a hand-writtenwindow.__gameglobal. The result is{ ticks, backend, state }, with anerrorfield when the readout threw. See Test state for agent play-testing.
Changed
BT.init: a call made while anotherinit()is still running now resolves tofalsewithout touching engine state, andBT.isReadystaysfalsefor the whole of anyinit(), including a re-init after an earlier success. A secondbootstrap()during init is refused with a logged error instead of racing the first. See Engine readiness.BT.captureFrame()called whileBT.renderAtholds 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'supdate(), on the tick clock (ticks * 1000 / targetFPS) instead of once per rendered frame onperformance.now(). They follow the fixed-step loop rather than wall time or the display's refresh rate, andBT.ticksReset()pauses them for an update instead of running them backwards. A palette write made inrender()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 sinceadd, including at tick 0. See Palette effects. - The
deltaMspassed to post-process effects is tick-clock time since the previous rendered frame, notperformance.now(). No built-in effect reads it. @blit386/kitandcreate-blit386ship 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 deniescheckout --,restore, andclean); Cursor loads the kit skills on its own; a.claude/launch.jsonfor the Claude desktop preview; the kit hooks format one file per edit and refuse agent edits to any.lockfile; the Biome schema version is no longer pinned in scaffolded games; generated Claude settings pre-approve theblit386-docsMCP server, andblit doctorwarns 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 aget_doc_pagetool with absolute URLs, publishes a build-a-game section, and ranks search by distinct matched terms (two-letter terms such asuiand2dnow 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 beforeupdate()could read it, soBT.isPressed(BT.BTN_POINTER_A)and the other pointer buttons missed it. The press now carries to the next frame that runsupdate(). 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 setpointerTypeexplicitly. See Test state for agent play-testing.
1.7.1 - 2026-09-27
Added
- Seeding from the URL: opening a page with
?seed=NseedsBT.randomautomatically, applied right before the demo's owninit()runs so aBT.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()andBT.downloadFrame()accept a new{ size: 'display' }option to capture the exact logicalBT.displaySizeframe instead of theBT.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 onpointerdown,pointerup,touchend, orkeydown, 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.isReducedMotionPreferredreads the browser'sprefers-reduced-motionsetting, and the optionalIBTDemo.onReducedMotionChange(prefersReduced)hook fires when it changes mid-session - the preference can flip while a game is running, the same way orientation can.?reducedmotionand?noreducedmotionare 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): writestransform(source[i], i)into slotstart + ifor every source color and returns the next free slot, so consecutive blocks chain. Each write delegates toset(), inheriting its validation including the rule that slot 0 stays transparent. It collapses thepalette.set(start + i, transform(baseColors[i]))loop that transformed sprite color blocks kept spelling out by hand. See Palette.window.BT: oncebootstrap()finishes, theBTnamespace is assigned towindow.BT, so the browser console can drive a running game (window.BT.captureFrame()). It followsBT.isDevModeby default, withBootstrapOptions.exposeGlobaloverriding in either direction, and the assignment goes through the engine's existingglobalThisguard so an SSR or Node import stays inert. See Core.BT.pointerPosTo(out, slot?)andBT.pointerDeltaTo(out, slot?): allocation-free counterparts topointerPos()/pointerDelta(), which mint a freshVector2ion 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 returnsVector2i.zero(), and the slot argument now trails the out param. See Input guide.BT.systemFont: a live reference to the sameBitmapFontinstanceBT.systemPrintdraws with, so it can be handed toBT.printFontor queried directly. Alongside it,BitmapFont#codePointsreturns every Unicode code point a font defines a glyph for, ascending, derived live from the same glyph mapgetGlyph()andhasGlyph()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.displaySizerather thanBT.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.isFrameCaptureShortcutEnabledgoverns them: unset means enabled in development and disabled in release,truekeeps them in a production build,falsefrees both keys. The publicBT.captureFrame()/BT.downloadFrame()are untouched and still matchBT.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), theBTdraw-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, andGameLoop'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.pointerDeltaToabove 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
Randomclass (a mulberry32 PRNG) plusBT.randomandBT.randomSeed(seed)on the facade.BT.randomis a live reference to one shared instance, time-seeded at singleton creation, so the engine default is deterministic the moment you seed it. Beyondnext()/float()/int()/intInclusive()it carries the draws games actually reach for -bool,sign,pick,shuffle/shuffleInPlace,weighted,angle,gaussian- and spatial helpersinsideRect,pointInRange,direction4,direction8, each with an allocation-free*To(out)variant. Stream control isseed,seedValue,getState,setState,clone, andfork. Prefer it overMath.random(), which cannot be seeded. See Random and Random guide. - Coordinate hashing:
hash1i/hash2i/hash3ireturn an unsigned 32-bit value andhash1/hash2/hash3return 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, andSimplexNoise, each seedable and sampling to approximately[-1, 1]. Value and Perlin exposenoise1D/noise2D/noise3Dand the matchingfbm*; Simplex is 2D and 3D only. fBm defaults tooctaves = 4,persistence = 0.5,lacunarity = 2, with octave amplitudes normalized so the sum stays in range. Distinct from the post-processNoisedisplay 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'sEaseclass. interpolate(easing, start, end, t)fornumber,Vector2i,Color32, andRect2i. Integer types round to the nearest component;Color32channels 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(0is a plain linear-light fade, default0.5) on the exportedExposureFadeOptionsbag. 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 theblit386/viteplugin'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.isSplashEnabledand the?splash/?nosplashURL flags to override either default. It doubles as a loading screen - the game'sinit()runs concurrently and the hold extends until it settles - and hands off into the game's palette with a single continuousBT.paletteFadeExposure, so there is no cut. Any key, click, or tap skips, and that press is swallowed. New gettersBT.splashState(aSplashState) andBT.isSplashVisible, plussplashColorDark/splashColorLightfor the ramp endpoints. The pixelated dissolve is WebGPU-only; the software backend gets the plain fade. See the splash guide and Core. BitmapFontfallback-glyph support: a.btfontfile (or the built-in system font) can include a glyph keyed byU+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: theU+FFFDfallback 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. Seescripts/system-font-extra-chars.mjsin the engine source for the full list and each glyph's atlas position.
Changed
BitmapFont.getGlyph()/getGlyphByCode()(andmeasureText(), which now shares their lookup) substitute the font's fallback glyph for a missing character instead of returningnulland 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 theblit386/viteplugin), an optionalIBTDemo.onHotReload(context)hook, and a tiered hot-swap - a method-only prototype swap when only method bodies changed, a full re-init wheninit()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 inblit386-demos/create-blit386ships in follow-up releases. blit386/vitedev-server plugin:import { blit386 } from 'blit386/vite'invite.config.tsappends the hot-reload registration snippet to a demo/game's entry module (skipped in production builds) and broadcasts asset changes under configured asset directories (defaultpublic/) asblit386:asset-changedHMR 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 onviteitself.- Engine-side asset hot-replace:
AssetLoader.evict, plus internal routing inHotRuntime.handleAssetChangedforblit386:asset-changedevents. A changed image, audio, or.btfontfile underpublic/updates the running demo in place - sprite sheets swap their texture (callingindexize()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 inblit386-demos/create-blit386ships in a follow-up release. - Asset loading progress:
BT.loadingAssetsCountis polled once per frame as a combined image+audio signal (the sum ofAssetLoader.loadingCountandAudioClip.loadingCount) to drive a loading spinner or progress bar until it returns to0.SpriteSheet.status('loading' | 'ready' | 'failed') andSpriteSheet.progressexpose per-sheet hot-reload replacement state;progressis coarse-grained (0or1.0, not a percentage).
Fixed
- Calling
bootstrap()a second time while already initialized no longer silently starts a second, unstoppableGameLoop. With theblit386/viteplugin installed and a hot-reload context registered, a second call now routes to a hot swap instead; without one, it logs an error and returnsfalse. - Running under
?backend=softwareno 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 genuineconfigure()change triggers a reload, matching the behavior already seen under the defaultwebgpubackend. - The
blit386/viteplugin now syntax-checks a plain.js/.mjsentry 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/.mtsentries are unaffected; they already get real syntax validation from Vite's own transform. canvas.style.touch-actionwas hardcoded tononeon attach, blocking touch tap-hold-scroll past the canvas even whenHardwareSettings.isCapturingPointerScrollwas left at its defaultfalse. It is now gated by the same flag:nonewhile pointer scroll capture is active (configure-time opt-in or the overlay's palette-band force),pan-yotherwise, updated live as either source of capture changes.
1.3.1 - 2026-07-19
Added
- Opt-in keyboard scroll capture via
HardwareSettings.isCapturingKeyboardScroll(defaultfalse). When enabled, canvaskeydownfor arrow keys, Space, PageUp/PageDown, Home, and End callspreventDefault()so games can map those keys without scrolling the host page. - Opt-in screen wake lock via
HardwareSettings.isWakeLockEnabled(defaultfalse). When enabled, the engine requests a screen wake lock after a successfulinit()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.screenOrientationreads the currentscreen.orientation.type(ornullwhen unavailable); optionalIBTDemo.onOrientationChange(type)fires on rotate;HardwareSettings.preferredOrientation('landscape' | 'portrait' | 'any', default'any') attemptsscreen.orientation.lock()after init. Lock failures are silent no-ops (for example on iOS Safari). HardwareSettings.isOverlayToggleHitDebugVisible(defaultfalse) draws a 1 pixel outline of the overlay's bottom-left17×13toggle hit region, for tuning the hit zone.
Changed
- Pointer wheel capture is opt-in via
HardwareSettings.isCapturingPointerScroll(defaultfalse). Canvas wheel events no longer callpreventDefault()by default, so host pages can scroll while the pointer is over the canvas. Demos that mapBT.pointerScrollDeltamust 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×48to17×13pixels, 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 (
sfxandmusicfeedmain, 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 theBT.isAudioUnlockedgetter. AudioClipasset: streamedfetchand 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-voiceBT.soundVolumeSet/BT.soundPitchSet/BT.soundPanSet(plus getters), and generationalSoundRefhandles.HardwareSettings.audioVoices(default16, range1-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 libraryBT.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 theBT.isMusicPlayinggetter. - Overlay audio meters: a per-bus level band with a voices used/total, steal, and drop readout, gated by
HardwareSettings.isOverlayAudioMetersEnabledwith lazy opt-in metering - no metering cost is paid unless the flag is set. BT.renderAlphagetter: the fractional interpolation factor in[0, 1)between fixedupdate()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_SRCcanvas usage, fixingBT.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:
KeyboardInputnow buffers press and release edges intopendingPress/pendingReleasequeues, consumed byendUpdate()at the fixed update boundary. On 120+ Hz displays with a 60 FPS target, fast taps between fixed update steps were previously dropped silently.endFrameis deprecated and forwards toendUpdate. - Stray
keyupevents 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.mdrewritten 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-techtoblit386. Update import paths andpackage.jsondependencies. - Breaking: Public demo interface renamed from
IBlitTechDemotoIBTDemo. - Breaking: Default canvas ID changed from
blit-tech-canvastoblit386-canvas; default frame-capture filename changed toblit386-capture.png.
Fixed
GPUTextureUsagewas 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
WebGPURendererwas imported statically, evaluatingGPUTextureUsageat 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.HardwareSettingsconfig flags renamed toisOverlayEnabled,isDetectingDroppedFrames, and so on. Deprecated aliases keep the previous names working - see the Deprecation Timeline. - Asset system structural refactor across
BitmapFont,SpriteSheet,Palette,PaletteEffect, andSystemFont- 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 viaBT.assignTag(). - WebGPU renderer pipeline diagnostics in the timing snapshot; a software-renderer diagnostics bar in the overlay.
BT.requestedBackendgetter for the resolved init backend, fixed even after a WebGPU-to-software fallback.
Changed
- Breaking:
HardwareSettings.rendererrenamed tobackend; the URL override is now?backend=software. - Breaking:
canvasDisplaySize/maxCanvasDisplaySizereplaced bydrawingBufferSize/maxCanvasSize. - Breaking: every
statsOverlay*setting, thestatsOverlayRows()hook, and theStatsOverlay*exported types drop thestats/StatsOverlayprefix (for exampleoverlayEnabled,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 partialHardwareSettingsobject - demos can override justtargetFPSwithout spelling out every field. The engine merges the result withdefaultConfig()via the new exportedmergeHardwareSettings().
Security
.btfontembedded textures must now usedata:image/png;base64with a 512 KiB payload cap; glyph metrics are validated before atlas decode begins. A newAssetLimitsmodule 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
BTread-only accessor -BT.displaySize()becomesBT.displaySize,BT.ticks()becomesBT.ticks, and so on acrossdeltaSeconds,timeSeconds,pointerScrollDelta,gamepadCount, andinputString. - Breaking: four accessors renamed -
fps()toBT.targetFPS,getActiveBackend()toBT.activeBackend,paletteGet()toBT.palette,cameraGet()toBT.camera.
Added
- New getters:
BT.canvasDisplaySizeandBT.outputSize(canvasDisplaySize ?? displaySize).
Security
RenderLimitsandAssetLimitscap 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
r8uintframebuffer stores one palette slot per pixel; aPaletteResolveUpscalePassconverts to RGBA immediately before output. - Canvas 2D
SoftwareRendererfallback, 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), andGamepadInput(dead-zone-filtered axes, per-player connectivity), all sharing theBTN_*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, andgreenpresets included. Paletteclass 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. Timerutility for fixed-timestep interval checks in update loops.
Changed
- Breaking:
IBlitTechGamerenamed toIBlitTechDemo;bootstrap()accepts a demo class rather than a game class; default canvas ID changed fromgame-canvastoblit-tech-canvas. - Breaking:
BT.initialize()andIBlitTechDemo.initialize()shortened toinit()across the entire engine lifecycle. - Breaking:
IBlitTechDemo.queryHardware()renamed toconfigure()and made optional; omitting it falls back todefaultConfig()(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. BTAPIsplit into three focused modules:GameLoop(fixed-timestep accumulator),WebGPUContext(adapter/device/context setup), delegated to byBTAPIitself.
Removed
- Obsolete WebGPU-only bootstrap helpers:
checkWebGPUSupport,detectBrowser,getWebGPUInstructions,buildErrorPreviewEntries,previewWebGPUErrors- superseded by theSoftwareRendererauto-fallback and a sharederrorMessages.tsmodule.
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
.btfontformat. ImageBitmapsupport and adestroy()method onSpriteSheet.- Enhancements to
Color32,Rect2i, andVector2i.
See also
GitHub Releases
Deprecation Timeline
Getting Started
API: Core
Last updated on October 18, 2026