diff --git a/src/BlitTech.ts b/src/BlitTech.ts index 56b86b1..ffe6729 100644 --- a/src/BlitTech.ts +++ b/src/BlitTech.ts @@ -42,7 +42,7 @@ import { PixelMosaic } from './render/effects/pixel/PixelMosaic'; import { amber, crtPipBoy, green } from './render/effects/presets'; import type { BootstrapOptions } from './utils/Bootstrap'; import { bootstrap } from './utils/Bootstrap'; -import { checkWebGPUSupport, displayError, getCanvas, previewWebGPUErrors } from './utils/BootstrapHelpers'; +import { displayError, getCanvas } from './utils/BootstrapHelpers'; import { clampCameraToWorld } from './utils/CameraUtils'; import { Color32 } from './utils/Color32'; import type { EasingFunction } from './utils/Easing'; @@ -1465,7 +1465,6 @@ export { BitmapFont, Bloom, bootstrap, - checkWebGPUSupport, ChromaticAberration, clampCameraToWorld, Color32, @@ -1480,7 +1479,6 @@ export { Palette, PixelGlitch, PixelMosaic, - previewWebGPUErrors, Rect2i, RGBMask, RollLine, diff --git a/src/utils/Bootstrap.test.ts b/src/utils/Bootstrap.test.ts index b212283..2935afe 100644 --- a/src/utils/Bootstrap.test.ts +++ b/src/utils/Bootstrap.test.ts @@ -5,7 +5,7 @@ * * Covers the DOM-facing startup flow: * - optional waiting for `DOMContentLoaded` - * - WebGPU support validation and canvas lookup failures + * - canvas lookup failures * - success and failure paths around `BTAPI.init()` * - default and custom canvas/container identifiers * - propagation of success and error callbacks @@ -60,18 +60,6 @@ function teardownDOM(): void { } } -function stubWebGPU(): void { - const nav = navigator as unknown as Record; - - nav.gpu = {}; -} - -function unstubWebGPU(): void { - const nav = navigator as unknown as Record; - - delete nav.gpu; -} - // #endregion describe('bootstrap', () => { @@ -82,9 +70,6 @@ describe('bootstrap', () => { afterEach(() => { vi.restoreAllMocks(); - - unstubWebGPU(); - teardownDOM(); }); @@ -94,8 +79,6 @@ describe('bootstrap', () => { it('should skip DOM waiting when waitForDOMReady is false', async () => { setupDOM(); - stubWebGPU(); - const result = await bootstrap(MockDemo, { waitForDOMReady: false }); expect(result).toBe(true); @@ -106,8 +89,6 @@ describe('bootstrap', () => { setupDOM(); - stubWebGPU(); - const bootstrapPromise = bootstrap(MockDemo, { waitForDOMReady: true }); Object.defineProperty(document, 'readyState', { value: 'complete', writable: true, configurable: true }); @@ -122,8 +103,6 @@ describe('bootstrap', () => { it('should proceed immediately when the readyState is complete', async () => { setupDOM(); - stubWebGPU(); - const result = await bootstrap(MockDemo, { waitForDOMReady: true }); expect(result).toBe(true); @@ -132,9 +111,9 @@ describe('bootstrap', () => { // #endregion - // #region WebGPU validation + // #region Backend fallback - describe('WebGPU validation', () => { + describe('backend fallback', () => { it('should proceed to engine init when WebGPU is not supported, letting BTAPI handle fallback', async () => { setupDOM(); @@ -146,32 +125,6 @@ describe('bootstrap', () => { expect(result).toBe(true); expect(onError).not.toHaveBeenCalled(); }); - - it('should skip WebGPU validation when ?renderer=software is set', async () => { - setupDOM(); - const originalLocation = window.location; - - try { - Object.defineProperty(window, 'location', { - configurable: true, - value: { - ...originalLocation, - search: '?renderer=software', - }, - }); - - const onError = vi.fn(); - const result = await bootstrap(MockDemo, { waitForDOMReady: false, onError }); - - expect(result).toBe(true); - expect(onError).not.toHaveBeenCalled(); - } finally { - Object.defineProperty(window, 'location', { - configurable: true, - value: originalLocation, - }); - } - }); }); // #endregion @@ -180,8 +133,6 @@ describe('bootstrap', () => { describe('canvas validation', () => { it('should return false and call onError when canvas is not found', async () => { - stubWebGPU(); - // No canvas in DOM — only the container. const container = document.createElement('div'); @@ -209,7 +160,6 @@ describe('bootstrap', () => { describe('engine initialization', () => { it('should return false and call onError when demo is missing render/update', async () => { setupDOM(); - stubWebGPU(); const initSpy = vi.spyOn(BTAPI.instance, 'init'); class BrokenDemo implements IBlitTechDemo { @@ -243,8 +193,6 @@ describe('bootstrap', () => { it('should return false and call onError when BTAPI.init returns false', async () => { setupDOM(); - stubWebGPU(); - vi.spyOn(BTAPI.instance, 'init').mockResolvedValue(false); const onError = vi.fn(); @@ -257,8 +205,6 @@ describe('bootstrap', () => { it('should return false and call onError when BTAPI.init throws', async () => { setupDOM(); - stubWebGPU(); - vi.spyOn(BTAPI.instance, 'init').mockRejectedValue(new Error('Init exploded')); const onError = vi.fn(); @@ -271,8 +217,6 @@ describe('bootstrap', () => { it('should return true and call onSuccess on successful initialization', async () => { setupDOM(); - stubWebGPU(); - const onSuccess = vi.fn(); const result = await bootstrap(MockDemo, { waitForDOMReady: false, onSuccess }); @@ -283,8 +227,6 @@ describe('bootstrap', () => { it('should set canvas tabIndex to 0 and focus the canvas before engine init', async () => { const { canvas } = setupDOM(); - stubWebGPU(); - const focusSpy = vi.spyOn(canvas, 'focus'); await bootstrap(MockDemo, { waitForDOMReady: false }); @@ -295,7 +237,6 @@ describe('bootstrap', () => { it('should use default canvas and container IDs when not specified', async () => { setupDOM(); - stubWebGPU(); const result = await bootstrap(MockDemo, { waitForDOMReady: false }); @@ -315,8 +256,6 @@ describe('bootstrap', () => { document.body.appendChild(canvas); - stubWebGPU(); - const result = await bootstrap(MockDemo, { waitForDOMReady: false, canvasId: 'custom-canvas', @@ -335,8 +274,6 @@ describe('bootstrap', () => { it('should return false when BTAPI.init throws synchronously', async () => { setupDOM(); - stubWebGPU(); - vi.spyOn(BTAPI.instance, 'init').mockImplementation(() => { throw new Error('The synchronous throw from init'); }); diff --git a/src/utils/Bootstrap.ts b/src/utils/Bootstrap.ts index 96487e6..484800d 100644 --- a/src/utils/Bootstrap.ts +++ b/src/utils/Bootstrap.ts @@ -196,7 +196,8 @@ async function initDemo( /** * One-liner bootstrap function for Blit-Tech demos. - * Handles WebGPU detection, canvas retrieval, and engine initialization. + * Handles canvas retrieval and engine initialization. Backend selection + * (WebGPU or software fallback) is managed internally by BTAPI. * * This function provides a streamlined way to start a demo with sensible defaults * while allowing customization through options. @@ -242,32 +243,22 @@ export async function bootstrap(DemoClass: DemoConstructor, options: BootstrapOp } try { - // BTAPI handles backend selection and auto-fallback to software when - // WebGPU is unavailable. Always proceed to initDemo() here. - // validateWebGPU and isSoftwareRendererQueryOverrideEnabled remain for - // VV-492 which will delete the now-obsolete helper code. - success = true; + const { canvas, result: canvasResult } = validateCanvas(canvasId, containerId, onError); - // Validate canvas element. - if (success) { - const { canvas, result: canvasResult } = validateCanvas(canvasId, containerId, onError); + success = canvasResult.success; - success = canvasResult.success; + if (success && canvas) { + canvas.tabIndex = 0; - // Initialize the demo. - if (success && canvas) { - canvas.tabIndex = 0; - - try { - canvas.focus({ preventScroll: true }); - } catch { - canvas.focus(); - } + try { + canvas.focus({ preventScroll: true }); + } catch { + canvas.focus(); + } - const initResult = await initDemo(DemoClass, canvas, containerId, onSuccess, onError); + const initResult = await initDemo(DemoClass, canvas, containerId, onSuccess, onError); - success = initResult.success; - } + success = initResult.success; } } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); diff --git a/src/utils/BootstrapHelpers.test.ts b/src/utils/BootstrapHelpers.test.ts index d8943c6..999ab04 100644 --- a/src/utils/BootstrapHelpers.test.ts +++ b/src/utils/BootstrapHelpers.test.ts @@ -2,22 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { - buildErrorPreviewEntries, - checkWebGPUSupport, - DEFAULT_CANVAS_ID, - DEFAULT_CONTAINER_ID, - detectBrowser, - displayError, - getCanvas, - getWebGPUInstructions, -} from './BootstrapHelpers'; -import { - CANVAS_NOT_FOUND_MESSAGE, - INIT_FAILED_MESSAGE, - WEBGPU_ADAPTER_MESSAGE, - WEBGPU_DEVICE_MESSAGE, -} from './errorMessages'; +import { DEFAULT_CANVAS_ID, DEFAULT_CONTAINER_ID, displayError, getCanvas } from './BootstrapHelpers'; // #region Constants @@ -34,27 +19,6 @@ describe('BootstrapHelpers', () => { // #endregion - // #region checkWebGPUSupport - - describe('checkWebGPUSupport', () => { - it('should return false when navigator.gpu is absent', () => { - expect(checkWebGPUSupport()).toBe(false); - }); - - it('should return true when navigator.gpu is present', () => { - const nav = navigator as unknown as { gpu?: unknown }; - nav.gpu = {}; - - try { - expect(checkWebGPUSupport()).toBe(true); - } finally { - delete nav.gpu; - } - }); - }); - - // #endregion - // #region getCanvas describe('getCanvas', () => { @@ -139,205 +103,4 @@ describe('BootstrapHelpers', () => { }); // #endregion - - // #region detectBrowser - - describe('detectBrowser', () => { - function withUA(ua: string, fn: () => void): void { - const original = navigator.userAgent; - Object.defineProperty(navigator, 'userAgent', { value: ua, configurable: true }); - - try { - fn(); - } finally { - Object.defineProperty(navigator, 'userAgent', { value: original, configurable: true }); - } - } - - it('should detect Chrome', () => { - withUA( - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - () => { - const result = detectBrowser(); - expect(result.name).toBe('chrome'); - expect(result.version).toBe(120); - }, - ); - }); - - it('should detect Edge (Chromium)', () => { - withUA( - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/113.0.0.0', - () => { - const result = detectBrowser(); - expect(result.name).toBe('edge'); - expect(result.version).toBe(113); - }, - ); - }); - - it('should detect Firefox stable', () => { - withUA('Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0', () => { - const result = detectBrowser(); - expect(result.name).toBe('firefox'); - expect(result.version).toBe(130); - }); - }); - - it('should detect Firefox Nightly', () => { - withUA( - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0 Nightly/20241201', - () => { - const result = detectBrowser(); - expect(result.name).toBe('firefox-nightly'); - expect(result.version).toBe(134); - }, - ); - }); - - it('should detect Safari', () => { - withUA( - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15', - () => { - const result = detectBrowser(); - expect(result.name).toBe('safari'); - expect(result.version).toBe(18); - }, - ); - }); - - it('should return unknown for an unrecognized user agent', () => { - withUA('Mozilla/5.0 (compatible; SomeUnknownBrowser/1.0)', () => { - const result = detectBrowser(); - expect(result.name).toBe('unknown'); - expect(result.version).toBe(0); - }); - }); - }); - - // #endregion - - // #region getWebGPUInstructions - - describe('getWebGPUInstructions', () => { - it('should advise updating Chrome when the version is below 113', () => { - const msg = getWebGPUInstructions({ name: 'chrome', version: 112 }); - expect(msg).toContain('Update Chrome'); - }); - - it('should point Chrome 113+ users to chrome://flags', () => { - const msg = getWebGPUInstructions({ name: 'chrome', version: 113 }); - expect(msg).toContain('chrome://flags'); - }); - - it('should advise updating Edge when the version is below 113', () => { - const msg = getWebGPUInstructions({ name: 'edge', version: 110 }); - expect(msg).toContain('Update Microsoft Edge'); - }); - - it('should point Edge 113+ users to edge://flags', () => { - const msg = getWebGPUInstructions({ name: 'edge', version: 113 }); - expect(msg).toContain('edge://flags'); - }); - - it('should tell Firefox Nightly users to use about:config', () => { - const msg = getWebGPUInstructions({ name: 'firefox-nightly', version: 134 }); - expect(msg).toContain('about:config'); - }); - - it('should tell Firefox stable users to download Nightly', () => { - const msg = getWebGPUInstructions({ name: 'firefox', version: 130 }); - expect(msg).toContain('Firefox Nightly'); - }); - - it('should advise updating Safari when the version is below 18', () => { - const msg = getWebGPUInstructions({ name: 'safari', version: 17 }); - expect(msg).toContain('Update Safari'); - }); - - it('should advise enabling WebGPU via Feature Flags for Safari 18-25', () => { - const msg = getWebGPUInstructions({ name: 'safari', version: 18 }); - expect(msg).toContain('Feature Flags'); - expect(msg).toContain('Develop'); - }); - - it('should mention macOS Tahoe 26 as the upgrade target for Safari 18-25', () => { - const msg = getWebGPUInstructions({ name: 'safari', version: 25 }); - expect(msg).toContain('Tahoe 26'); - }); - - it('should say WebGPU is enabled by default for Safari 26+', () => { - const msg = getWebGPUInstructions({ name: 'safari', version: 26 }); - expect(msg).toContain('enabled by default'); - expect(msg).toContain('Feature Flags'); - }); - - it('should list supported browsers for an unknown browser', () => { - const msg = getWebGPUInstructions({ name: 'unknown', version: 0 }); - expect(msg).toContain('Chrome 113'); - }); - }); - - // #endregion - - // #region buildErrorPreviewEntries - - describe('buildErrorPreviewEntries', () => { - it('canvas-not-found entry uses CANVAS_NOT_FOUND_MESSAGE', () => { - const entries = buildErrorPreviewEntries(); - const entry = entries.find((e) => e.label === 'Canvas element not found'); - - expect(entry).toBeDefined(); - expect(entry?.content).toBe(CANVAS_NOT_FOUND_MESSAGE(DEFAULT_CANVAS_ID)); - }); - - it('init-failed entry text uses INIT_FAILED_MESSAGE', () => { - const entries = buildErrorPreviewEntries(); - const entry = entries.find((e) => e.label === 'Initialization failed (with error)'); - - expect(entry).toBeDefined(); - - const content = entry?.content; - - expect(typeof content).toBe('object'); - - if (typeof content === 'object' && content !== null && 'text' in content) { - expect(content.text).toBe(INIT_FAILED_MESSAGE); - } - }); - - it('adapter-unavailable entry uses WEBGPU_ADAPTER_MESSAGE in code slot', () => { - const entries = buildErrorPreviewEntries(); - const entry = entries.find((e) => e.label === 'WebGPU adapter unavailable'); - - expect(entry).toBeDefined(); - - const content = entry?.content; - - expect(typeof content).toBe('object'); - - if (typeof content === 'object' && content !== null && 'code' in content) { - expect(content.code).toBe(WEBGPU_ADAPTER_MESSAGE); - expect((content as { text: string }).text).toBe(INIT_FAILED_MESSAGE); - } - }); - - it('device-unavailable entry uses WEBGPU_DEVICE_MESSAGE in code slot', () => { - const entries = buildErrorPreviewEntries(); - const entry = entries.find((e) => e.label === 'WebGPU device unavailable'); - - expect(entry).toBeDefined(); - - const content = entry?.content; - - expect(typeof content).toBe('object'); - - if (typeof content === 'object' && content !== null && 'code' in content) { - expect(content.code).toBe(WEBGPU_DEVICE_MESSAGE); - expect((content as { text: string }).text).toBe(INIT_FAILED_MESSAGE); - } - }); - }); - - // #endregion }); diff --git a/src/utils/BootstrapHelpers.ts b/src/utils/BootstrapHelpers.ts index c80a048..f702305 100644 --- a/src/utils/BootstrapHelpers.ts +++ b/src/utils/BootstrapHelpers.ts @@ -1,8 +1,7 @@ /** * DOM utility helpers for Blit-Tech bootstrap. * - * These utilities are independent of the engine and can be used directly - * in demos or host pages for WebGPU detection and error display. + * Canvas lookup and error display utilities used by the bootstrap function. */ // #region Constants @@ -13,38 +12,6 @@ export const DEFAULT_CANVAS_ID = 'blit-tech-canvas'; /** Default container element ID for error display. */ export const DEFAULT_CONTAINER_ID = 'canvas-container'; -/** Minimum Chrome/Edge major version that supports WebGPU. */ -const MIN_CHROME_EDGE_VERSION = 113; - -/** Minimum Safari major version where WebGPU is available (behind Feature Flags on 18-25, default on 26+). */ -const MIN_SAFARI_VERSION = 18; - -/** Minimum Safari major version where WebGPU is enabled by default. */ -const MIN_SAFARI_DEFAULT_VERSION = 26; - -/** Minimum Firefox major version that ships WebGPU enabled by default on Windows. */ -const MIN_FIREFOX_VERSION = 141; - -/** Download URL for Chrome. */ -const DOWNLOAD_CHROME_URL = 'https://www.google.com/chrome'; - -/** Download URL for Firefox Nightly. */ -const FIREFOX_NIGHTLY_URL = 'https://www.mozilla.org/firefox/channel/desktop/'; - -// #endregion - -import { - CANVAS_NOT_FOUND_MESSAGE, - INIT_FAILED_MESSAGE, - WEBGPU_ADAPTER_MESSAGE, - WEBGPU_DEVICE_MESSAGE, -} from './errorMessages'; - -// #region Module State - -/** Stored keydown handler for previewWebGPUErrors, used to remove the previous listener on re-entry. */ -let previewKeyHandler: ((e: KeyboardEvent) => void) | null = null; - // #endregion // #region Types @@ -60,59 +27,10 @@ export type ErrorContent = code?: string; }; -/** - * Detected browser identity and major version. - */ -export interface BrowserInfo { - /** Detected browser name. 'unknown' when the UA cannot be matched. */ - name: 'chrome' | 'edge' | 'firefox' | 'firefox-nightly' | 'safari' | 'unknown'; - - /** Major version number, or 0 when the version cannot be parsed. */ - version: number; -} - // #endregion // #region Internal Helpers -/** - * Parses a major version number from a user-agent token like "Chrome/120.0.0.0". - * - * @param ua - Full user-agent string. - * @param token - Token prefix to search for, e.g. "Chrome/". - * @returns Major version number, or 0 if not found. - */ -function parseMajorVersion(ua: string, token: string): number { - const idx = ua.indexOf(token); - - if (idx === -1) { - return 0; - } - - const version = parseInt(ua.slice(idx + token.length), 10); - - return Number.isNaN(version) ? 0 : version; -} - -// #endregion - -// #region Exported Helpers - -/** - * Checks if WebGPU is supported in the current browser environment. - * Tests for the presence of the navigator.gpu API. - * - * @returns True if WebGPU is available, false otherwise. - * - * @example - * if (!checkWebGPUSupport()) { - * displayError('WebGPU Not Supported', 'Please use Chrome 113+'); - * } - */ -export function checkWebGPUSupport(): boolean { - return typeof navigator !== 'undefined' && 'gpu' in navigator; -} - /** * Appends text to an element, converting newline characters to `
` elements. * Safe against XSS: all text is inserted via createTextNode, never innerHTML. @@ -132,6 +50,10 @@ function appendTextWithLineBreaks(element: HTMLElement, text: string): void { }); } +// #endregion + +// #region Exported Helpers + /** * Displays an error message in the page UI. * Replaces the container's content with a styled error box. @@ -222,171 +144,6 @@ export function displayError(title: string, content: ErrorContent, containerId: } } -/** - * Detects the current browser from the navigator user-agent string. - * - * Detection order matters: Edge UAs also contain "Chrome", so Edge is checked first. - * Firefox Nightly UAs contain both "Firefox" and "Nightly". - * Safari UAs contain "Safari" and "Version" but not "Chrome" or "Edg". - * - * @returns Detected browser name and major version. - * - * @example - * const { name, version } = detectBrowser(); - * // name: 'chrome', version: 120 - */ -export function detectBrowser(): BrowserInfo { - if (typeof navigator === 'undefined') { - return { name: 'unknown', version: 0 }; - } - - const ua = typeof navigator.userAgent === 'string' ? navigator.userAgent : ''; - - // Edge must be checked before Chrome — Edge UAs also contain "Chrome/". - if (ua.includes('Edg/')) { - return { name: 'edge', version: parseMajorVersion(ua, 'Edg/') }; - } - - if (ua.includes('Chrome/')) { - return { name: 'chrome', version: parseMajorVersion(ua, 'Chrome/') }; - } - - // Firefox Nightly UAs contain "Nightly" in addition to "Firefox/". - if (ua.includes('Firefox/')) { - if (ua.includes('Nightly')) { - return { name: 'firefox-nightly', version: parseMajorVersion(ua, 'Firefox/') }; - } - - return { name: 'firefox', version: parseMajorVersion(ua, 'Firefox/') }; - } - - // Safari UAs contain "Version/X" for the Safari release version. - // Chrome/Edge also include "Safari/" so they must already be ruled out above. - if (ua.includes('Safari/') && ua.includes('Version/')) { - return { name: 'safari', version: parseMajorVersion(ua, 'Version/') }; - } - - return { name: 'unknown', version: 0 }; -} - -/** - * Returns actionable, browser-specific instructions for enabling WebGPU. - * - * The message is plain text suitable for display in the error panel. - * - * @param browser - Detected browser from {@link detectBrowser}. - * @returns Human-readable instructions string. - * - * @example - * const info = detectBrowser(); - * const instructions = getWebGPUInstructions(info); - * displayError('WebGPU Not Available', instructions); - */ -export function getWebGPUInstructions(browser: BrowserInfo): string { - switch (browser.name) { - case 'chrome': - if (browser.version < MIN_CHROME_EDGE_VERSION) { - return ( - `Update Chrome to version ${MIN_CHROME_EDGE_VERSION} or later to use WebGPU.\n` + - `Download the latest version at ${DOWNLOAD_CHROME_URL}` - ); - } - - return ( - 'WebGPU is supported in this version of Chrome but is not available.\n\n' + - '1. Check that hardware acceleration is on: open chrome://settings/system in the address bar ' + - 'and confirm "Use graphics acceleration when available" is enabled, then relaunch Chrome.\n' + - '2. Make sure the page is served over https or http://localhost, not plain http.\n' + - '3. If your GPU is blocklisted, open chrome://flags/#enable-unsafe-webgpu, ' + - 'set "Unsafe WebGPU Support" to Enabled, and relaunch Chrome.' - ); - - case 'edge': - if (browser.version < MIN_CHROME_EDGE_VERSION) { - return `Update Microsoft Edge to version ${MIN_CHROME_EDGE_VERSION} or later to use WebGPU.`; - } - - return ( - 'WebGPU is supported in this version of Edge but is not available.\n\n' + - '1. Check that hardware acceleration is on: open edge://settings/system in the address bar ' + - 'and confirm "Use hardware acceleration when available" is enabled, then relaunch Edge.\n' + - '2. Make sure the page is served over https or http://localhost, not plain http.\n' + - '3. If your GPU is blocklisted, open edge://flags/#enable-unsafe-webgpu, ' + - 'set "Unsafe WebGPU Support" to Enabled, and relaunch Edge.' - ); - - case 'firefox-nightly': - return ( - 'Enable WebGPU in Firefox Nightly:\n\n' + - '1. Type about:config in the address bar and press Enter.\n' + - '2. Accept the risk warning to proceed.\n' + - '3. Search for dom.webgpu.enabled.\n' + - '4. Double-click the entry to set it to true.\n' + - '5. Restart Firefox Nightly.' - ); - - case 'firefox': - if (browser.version >= MIN_FIREFOX_VERSION) { - return ( - 'WebGPU support in Firefox depends on your operating system:\n\n' + - `- Windows: enabled by default in Firefox ${MIN_FIREFOX_VERSION}+.\n` + - '- Mac (Apple Silicon): enabled by default in Firefox 145+ on macOS 26, ' + - 'or Firefox 147+ on all macOS versions.\n' + - '- Linux and Android: available in Firefox Nightly only, ' + - 'not yet in stable.\n\n' + - 'If you are on a supported OS and WebGPU is still unavailable, ' + - 'open about:config, confirm dom.webgpu.enabled is set to true, ' + - 'and restart Firefox.' - ); - } - - return ( - 'WebGPU is not yet available in this version of Firefox.\n' + - `Update to Firefox ${MIN_FIREFOX_VERSION} or later (Windows), or download Firefox Nightly ` + - `at ${FIREFOX_NIGHTLY_URL} and enable dom.webgpu.enabled in about:config.` - ); - - case 'safari': - if (browser.version < MIN_SAFARI_VERSION) { - return ( - `Update Safari to version ${MIN_SAFARI_VERSION} or later to use WebGPU. ` + - `Safari ${MIN_SAFARI_VERSION}–${MIN_SAFARI_DEFAULT_VERSION - 1} supports WebGPU via Feature Flags; ` + - `Safari ${MIN_SAFARI_DEFAULT_VERSION}+ (macOS Tahoe 26) has it enabled by default.` - ); - } - - if (browser.version < MIN_SAFARI_DEFAULT_VERSION) { - return ( - `Safari ${MIN_SAFARI_VERSION}-${MIN_SAFARI_DEFAULT_VERSION - 1} supports WebGPU but it must be enabled manually. ` + - 'To enable WebGPU in Safari:\n\n' + - '1. Open Safari Settings (Command + Comma) and go to the Advanced tab.\n' + - '2. Check "Show features for web developers" to enable the Develop menu.\n' + - '3. From the menu bar, choose Develop > Feature Flags.\n' + - '4. Search for "WebGPU" and enable it.\n' + - '5. Restart Safari.\n\n' + - `Alternatively, update to Safari ${MIN_SAFARI_DEFAULT_VERSION} (macOS Tahoe 26) where WebGPU is on by default.` - ); - } - - return ( - `WebGPU is enabled by default in Safari ${MIN_SAFARI_DEFAULT_VERSION} but appears to be unavailable. ` + - 'If you disabled it via Feature Flags, re-enable it:\n\n' + - '1. Open Safari Settings (Command + Comma) and go to the Advanced tab.\n' + - '2. Check "Show features for web developers" to enable the Develop menu.\n' + - '3. From the menu bar, choose Develop > Feature Flags.\n' + - '4. Search for "WebGPU" and enable it.\n' + - '5. Restart Safari.' - ); - - default: - return ( - `Supported browsers: Chrome ${MIN_CHROME_EDGE_VERSION}+, Microsoft Edge ${MIN_CHROME_EDGE_VERSION}+, ` + - `Firefox ${MIN_FIREFOX_VERSION}+ (Windows / Mac 145+) or Firefox Nightly, ` + - `Safari ${MIN_SAFARI_VERSION}+ (${MIN_SAFARI_VERSION}–${MIN_SAFARI_DEFAULT_VERSION - 1} via Feature Flags, ${MIN_SAFARI_DEFAULT_VERSION}+ by default).` - ); - } -} - /** * Retrieves a canvas element from the DOM by ID. * Validates that the element exists and is a canvas element. @@ -417,235 +174,3 @@ export function getCanvas(canvasId: string = DEFAULT_CANVAS_ID): HTMLCanvasEleme } // #endregion - -// #region Dev Utilities - -/** - * Preview entry describing a single error variant. - */ -interface ErrorPreviewEntry { - /** Short human-readable label shown in the navigation bar. */ - readonly label: string; - - /** Error panel heading. */ - readonly title: string; - - /** Error panel body content. */ - readonly content: ErrorContent; -} - -/** - * Returns all distinct error message variants used by the engine. - * Covers every browser/version branch of {@link getWebGPUInstructions} plus the - * non-WebGPU error types (canvas, initialization, adapter, device, unexpected). - * - * Exported for testing: the test suite verifies that canvas / init-failed / - * adapter / device entries use the exact same strings as production bootstrap. - * - * @returns Array of preview entries, one per error variant. - */ -export function buildErrorPreviewEntries(): ReadonlyArray { - return [ - // WebGPU not supported — Chrome - { - label: 'Chrome, outdated (< 113)', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'chrome', version: 100 }), - }, - { - label: 'Chrome, current (>= 113)', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'chrome', version: 130 }), - }, - - // WebGPU not supported — Edge - { - label: 'Edge, outdated (< 113)', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'edge', version: 100 }), - }, - { - label: 'Edge, current (>= 113)', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'edge', version: 130 }), - }, - - // WebGPU not supported — Firefox - { - label: 'Firefox Nightly', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'firefox-nightly', version: 130 }), - }, - { - label: 'Firefox stable, outdated (< 141)', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'firefox', version: 130 }), - }, - { - label: 'Firefox stable, current (>= 141)', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'firefox', version: 141 }), - }, - - // WebGPU not supported — Safari - { - label: 'Safari, outdated (< 18)', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'safari', version: 17 }), - }, - { - label: 'Safari 18-25, Feature Flags required', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'safari', version: 18 }), - }, - { - label: 'Safari 26+, enabled by default', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'safari', version: 26 }), - }, - - // WebGPU not supported — unknown browser - { - label: 'Unknown browser', - title: 'WebGPU Not Supported', - content: getWebGPUInstructions({ name: 'unknown', version: 0 }), - }, - - // Non-WebGPU errors - { - label: 'Canvas element not found', - title: 'Canvas Error', - content: CANVAS_NOT_FOUND_MESSAGE(DEFAULT_CANVAS_ID), - }, - { - label: 'Initialization failed (with error)', - title: 'Initialization Failed', - content: { - text: INIT_FAILED_MESSAGE, - code: "TypeError: Cannot read properties of undefined (reading 'requestDevice')", - }, - }, - { - label: 'WebGPU adapter unavailable', - title: 'Initialization Failed', - content: { - text: INIT_FAILED_MESSAGE, - code: WEBGPU_ADAPTER_MESSAGE, - }, - }, - { - label: 'WebGPU device unavailable', - title: 'Initialization Failed', - content: { - text: INIT_FAILED_MESSAGE, - code: WEBGPU_DEVICE_MESSAGE, - }, - }, - { - label: 'Unexpected error during bootstrap', - title: 'Unexpected Error', - content: { - text: 'An unexpected error occurred during initialization:', - code: 'RangeError: Maximum call stack size exceeded', - }, - }, - ]; -} - -/** - * Cycles through every error message variant for visual testing. - * - * Renders each variant in the error container with Prev/Next navigation buttons. - * Arrow keys also cycle through variants once the function has been called. - * - * Intended for **development use only**. Do not call this in production demos. - * - * @param containerId - Container element ID. Default: 'canvas-container' - * - * @example - * // Call once during development to inspect all error layouts: - * import { previewWebGPUErrors } from 'blit-tech'; - * previewWebGPUErrors(); - */ -export function previewWebGPUErrors(containerId: string = DEFAULT_CONTAINER_ID): void { - // Bail out before touching the global key handler if the container is missing. - if (!document.getElementById(containerId)) { - return; - } - - const entries = buildErrorPreviewEntries(); - let current = 0; - - const show = (index: number): void => { - current = index; - - // eslint-disable-next-line security/detect-object-injection - const entry = entries[index]; - - if (!entry) { - return; - } - - displayError(entry.title, entry.content, containerId); - - const container = document.getElementById(containerId); - - if (!container) { - return; - } - - // Navigation bar appended below the error box. - const nav = document.createElement('div'); - - nav.style.cssText = - 'display: flex; align-items: center; justify-content: center; gap: 16px; ' + - 'margin-top: 12px; font-family: monospace; font-size: 12px; color: #aaa;'; - - const prevBtn = document.createElement('button'); - - prevBtn.type = 'button'; - prevBtn.textContent = '<< Prev'; - prevBtn.style.cssText = 'padding: 4px 12px; font-family: monospace; cursor: pointer;'; - prevBtn.addEventListener('click', () => { - show((current - 1 + entries.length) % entries.length); - }); - - const counter = document.createElement('span'); - - counter.textContent = `${index + 1} / ${entries.length} — ${entry.label}`; - - const nextBtn = document.createElement('button'); - - nextBtn.type = 'button'; - nextBtn.textContent = 'Next >>'; - nextBtn.style.cssText = 'padding: 4px 12px; font-family: monospace; cursor: pointer;'; - nextBtn.addEventListener('click', () => { - show((current + 1) % entries.length); - }); - - nav.appendChild(prevBtn); - nav.appendChild(counter); - nav.appendChild(nextBtn); - container.appendChild(nav); - }; - - // Arrow-key navigation. Remove any previously registered handler first so - // repeated calls don't stack multiple listeners. - if (previewKeyHandler) { - document.removeEventListener('keydown', previewKeyHandler); - } - - previewKeyHandler = (e: KeyboardEvent): void => { - if (e.key === 'ArrowLeft') { - show((current - 1 + entries.length) % entries.length); - } else if (e.key === 'ArrowRight') { - show((current + 1) % entries.length); - } - }; - - document.addEventListener('keydown', previewKeyHandler); - - show(current); -} - -// #endregion -- 2.54.0 From 925f1c9b5034f2580de4011d87da5153290182b6 Mon Sep 17 00:00:00 2001 From: Vaclav Vancura Date: Sat, 9 May 2026 17:39:47 +0200 Subject: [PATCH 2/2] docs(api): update README and CLAUDE.md to remove checkWebGPUSupport references Remove checkWebGPUSupport from the Quick Start manual-init code example and the Bootstrap Utilities API reference table. Replace the old guard pattern (check WebGPU then init) with a BT.init + BT.getActiveBackend() example that reflects the current auto-fallback model. Update inline comments and section prose that described bootstrap as doing "WebGPU detection". Update BootstrapHelpers.ts description in both README structure tree and CLAUDE.md. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Vaclav Vancura --- CLAUDE.md | 2 +- README.md | 31 ++++++++++++++----------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 946f0fd..2fb249c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ src/ defaultKeyboardMap.ts # Default face-button key tables; clone helpers for BT.inputMapReset utils/ Bootstrap.ts # Demo bootstrap utilities - BootstrapHelpers.ts # WebGPU detection, canvas lookup, error display + BootstrapHelpers.ts # Canvas lookup and error display utilities CameraUtils.ts # Camera clamp helper (world/view bounds) Vector2i.ts # Integer 2D vector Rect2i.ts # Integer rectangle diff --git a/README.md b/README.md index 76eccc2..6cc7664 100644 --- a/README.md +++ b/README.md @@ -204,25 +204,23 @@ class MyDemo implements IBlitTechDemo { } } -// One-liner bootstrap - handles WebGPU detection, DOM ready, and error display +// One-liner bootstrap - handles DOM ready, canvas lookup, backend selection, and error display bootstrap(MyDemo); ``` -For more control over initialization: +For more control over initialization (without `bootstrap`): ```ts -import { BT, checkWebGPUSupport, displayError, getCanvas } from '../src/BlitTech'; - -// Manual initialization with custom error handling -if (!checkWebGPUSupport()) { - displayError( - 'WebGPU Not Supported', - 'Please use a WebGPU-compatible browser (Chrome/Edge 113+, Firefox 141+ on Windows, Safari 18+ with Feature Flags or Safari 26+).', - ); -} else { - const canvas = getCanvas('my-canvas-id'); - if (canvas) { - await BT.init(new MyDemo(), canvas); +import { BT, displayError, getCanvas } from '../src/BlitTech'; + +// Manual initialization — BT.init selects WebGPU or software fallback automatically +const canvas = getCanvas('my-canvas-id'); +if (canvas) { + const success = await BT.init(new MyDemo(), canvas); + if (success) { + console.log('Backend:', BT.getActiveBackend()); // 'webgpu' or 'software' + } else { + displayError('Initialization Failed', 'Engine could not start.'); } } ``` @@ -265,7 +263,7 @@ blit-tech/ │ │ └── defaultKeyboardMap.ts # Default face-button key tables (VV-435) │ ├── utils/ │ │ ├── Bootstrap.ts # Demo bootstrap utilities -│ │ ├── BootstrapHelpers.ts # WebGPU detection, error display +│ │ ├── BootstrapHelpers.ts # Canvas lookup and error display utilities │ │ ├── Color32.ts # 32-bit RGBA color │ │ ├── Easing.ts # Easing functions for palette effects │ │ ├── FrameCapture.ts # GPU readback + PNG export @@ -290,7 +288,7 @@ blit-tech/ ### Bootstrap Utilities -The bootstrap utilities provide a streamlined way to initialize demos with automatic WebGPU detection and error +The bootstrap utilities provide a streamlined way to initialize demos with automatic backend selection and error handling: ```ts @@ -306,7 +304,6 @@ bootstrap(MyDemo, { }); // Individual utilities for manual control -checkWebGPUSupport(); // Returns true if WebGPU is available displayError(title, message, containerId?); // Show styled error in DOM getCanvas(canvasId?); // Get canvas element safely ```