From 7171f4e7b9976326e9ce930c3c4f7ff4c45dd326 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Tue, 8 Sep 2026 17:05:01 -0400 Subject: [PATCH] feat!: upgrade to @solidtv/renderer 1.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renderer 1.8 was a breaking release: render-to-texture, the per-node boundsMargin prop, the inBounds/outOfBounds events and container autosize were all removed, and 1.9 dropped the __calculateFps__ build gate. Move the framework onto 1.9 and bring the docs in line. Code: - Drop `rtt` from ElementNode's prop list, its color default, and the DOM renderer's props, accessors and texture-type switch. RTT has no replacement, so FadeInOut (which flattened its subtree before fading out) and Marquee (which drew its scrolling copies through a texture) now render directly. Both are visible behavior changes. - Remove the per-node `boundsMargin` prop from the DOM renderer and make `normalizeBoundsMargin` return a scalar, mirroring the WebGL renderer's largest-edge coercion and warning for the dropped tuple form. - Replace the `inBounds`/`outOfBounds` node events with `inViewport`/`outOfViewport`, typed as `NodeViewportPayload`, and emit them from the DOM renderer with the same semantics as CoreNode. - Guard the DOM renderer's image path on `typeof src === 'string'` now that the renderer accepts `Blob | ImageData` sources it cannot paint. - Remove the createTag primitive and its docs. It existed only to build RTT textures and cannot work on 1.9. `autosize` needed no change: solid only ever used it on the texture-load path, which is the mode that survived. Also fixes Config.rendererOptions, which was a union of the WebGL and DOM settings types and so hid every member the two did not share. It is now an intersection, which drops the `'inspector' in ...` narrowing dance and lets callers read `devicePhysicalPixelRatio` directly. Docs: new upgrade guide, rewritten build-flag article (five surviving flags, not seven), and corrected defaults throughout — boundsMargin 0 -> 200, numImageWorkers 2 -> 1, quadBufferSize 1310720 -> 1048576, targetFPS now capped at 60. Co-Authored-By: Claude Opus 5 --- docs/_sidebar.md | 2 +- docs/articles/basics.md | 5 +- docs/articles/renderer-1.9-upgrade.md | 171 ++++++++++++++++++++++ docs/articles/solidtv_renderer.md | 72 +++++---- docs/essentials/events.md | 21 ++- docs/essentials/images.md | 4 + docs/essentials/render.md | 26 +++- docs/essentials/styling.md | 8 +- docs/primitives/createTag.md | 69 --------- docs/primitives/fpscounter.md | 27 ++++ package.json | 2 +- pnpm-lock.yaml | 10 +- src/core/config.ts | 8 +- src/core/dom-renderer/domRenderer.ts | 39 ++--- src/core/dom-renderer/domRendererTypes.ts | 12 +- src/core/dom-renderer/domRendererUtils.ts | 45 +++--- src/core/elementNode.ts | 20 +-- src/core/intrinsicTypes.ts | 6 +- src/primitives/FadeInOut.tsx | 1 - src/primitives/Marquee.tsx | 4 +- src/primitives/createTag.tsx | 39 ----- src/primitives/index.ts | 1 - 22 files changed, 359 insertions(+), 233 deletions(-) create mode 100644 docs/articles/renderer-1.9-upgrade.md delete mode 100644 docs/primitives/createTag.md delete mode 100644 src/primitives/createTag.tsx diff --git a/docs/_sidebar.md b/docs/_sidebar.md index b126e958..30e6aca9 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -2,6 +2,7 @@ - [Basics](/articles/basics.md) - [Rendering Hello World](/essentials/render.md) - [Migration 2.x to 3.0](/articles/migration-2x-to-3.0.md) + - [Upgrading to Renderer 1.9](/articles/renderer-1.9-upgrade.md) - Core Concepts - [Components](/essentials/components.md) - [ElementNode](/essentials/elementnode.md) @@ -37,7 +38,6 @@ - [Image Component](/primitives/image.md) - [borderBox](/primitives/borderBox.md) - [createBlurredImage](/primitives/createBlurredImage.md) - - [createTag](/primitives/createTag.md) - [FPS Counter](/primitives/fpscounter.md) - [LazyUp](/primitives/lazyUp.md) - [Marquee](/primitives/marquee.md) diff --git a/docs/articles/basics.md b/docs/articles/basics.md index 02f4ebc8..f0f7f25a 100644 --- a/docs/articles/basics.md +++ b/docs/articles/basics.md @@ -39,14 +39,14 @@ Config.fontSettings.color = 0xffffffff; // Settings for SolidTV Renderer passed in for starting application Config.rendererOptions = { - numImageWorkers: 2, + numImageWorkers: 1, fontEngines: [SdfTextRenderer], renderEngine: WebGlCoreRenderer, inspector: Inspector, // Set the resolution based on window height deviceLogicalPixelRatio: window.innerHeight / 1080, devicePhysicalPixelRatio: 1, - boundsMargin: 20, + boundsMargin: 200, }; ``` @@ -54,6 +54,7 @@ Here, we’re setting up a few important things: - We’re defaulting font settings for all `` nodes. - The `rendererOptions` allow us to pass options to the SolidTV renderer, including the number of image workers, font engines, and pixel ratios. +- `boundsMargin` is the preload margin around the viewport, in logical pixels. It defaults to `200`, which is usually what you want — it loads textures ahead of a node scrolling into view. ## Routing with SolidJS Router diff --git a/docs/articles/renderer-1.9-upgrade.md b/docs/articles/renderer-1.9-upgrade.md new file mode 100644 index 00000000..6a144861 --- /dev/null +++ b/docs/articles/renderer-1.9-upgrade.md @@ -0,0 +1,171 @@ +# Upgrading to Renderer 1.9 + +`@solidtv/solid` now requires `@solidtv/renderer` 1.9. The 1.8 release was a +breaking one: four features were removed or changed shape, the telemetry payload +was reworked, and three build-time defines disappeared. This page covers what an +app built on `@solidtv/solid` has to change. + +Bump the peer dependency and install: + +```json +"@solidtv/renderer": "^1.9.0" +``` + +## Render-to-texture (`rtt`) is gone + +The `rtt` prop, the `RenderTexture` texture type and the `__RTT__` build define +were removed in full. There is no replacement and no compatibility shim. + +```jsx +// before +... + +// after — nothing to migrate to +... +``` + +If a subtree relied on being flattened into a texture, the honest options are to +drop the effect, or to pre-render the subtree as an image asset and use that. + +Two framework primitives used `rtt` internally and no longer do: + +- **`FadeInOut`** flattened the subtree before its fade-out animation. Alpha is + now composited per node, so children that overlap each other can show seams + through the fade. +- **`Marquee`** rendered its scrolling text copies through a texture. They are + now drawn directly. + +The `createTag` primitive, which existed only to build RTT textures, was removed +from `@solidtv/solid/primitives`. + +## Bounds and viewport + +### `boundsMargin` is a stage setting, not a node prop + +The per-node `boundsMargin` prop is gone. The renderer setting remains, but is +now a single `number` — the `[top, right, bottom, left]` array form still parses +(the largest edge wins, with a console warning) and should be replaced. + +**Its default changed from `0` to `200`.** An app that never set it now gets a +200px preload margin it did not have before. That is intended — its only job is +to load textures ahead of a node scrolling into view — but it does raise peak +texture memory. + +```js +Config.rendererOptions = { + boundsMargin: 200, +}; +``` + +### `inBounds` / `outOfBounds` events were replaced + +Use `inViewport` and `outOfViewport`. `outOfViewport` fires when a node leaves +the viewport, covering both the `InViewport -> InBounds` and +`InViewport -> OutOfBounds` transitions. + +```jsx +// before + + +// after + +``` + +A listener on `inBounds` was firing one step earlier than viewport entry. If it +was starting a load, that job now belongs to `boundsMargin`; if it was showing +something, it was already too early and `inViewport` is what you wanted. + +Both events carry a `{ previous, current }` payload of `CoreNodeRenderState` +values, and both are gated behind `__emitBoundsEvents__` — set that define to +`true` if your app listens to them. + +### `renderOnlyInViewport` was removed + +Its `true` behavior is now unconditional. An app that set it to `false` was +drawing everything regardless of viewport and will now draw only what is in +view. + +## `autosize` is texture-only + +`autosize` now means only "take the texture's intrinsic dimensions when it +loads". It no longer sizes a node to its children, and it is a no-op on `` +nodes. + +- A node with `src` and no children: unchanged. +- A container relying on `autosize` to measure its content: this silently stops + resizing and keeps whatever `w`/`h` it was given, frequently `0`, so the + subtree can disappear or stop clipping. Use flex (`display: 'flex'`) to size + containers, or set the size yourself. +- A `` node: remove the prop. + +## Texture and image loading + +- **`numImageWorkers` is clamped to at most 1** and defaults to `1` (was `2`). + Passing `2` or `4` silently yields `1`. The pool measured 99.7% idle, so this + is not a regression to work around; set it to `1`, or `0` to keep image + loading on the main thread. +- **`quadBufferSize` defaults to `1048576`** (was `1310720`). Set it explicitly + if your app relied on the old default and draws an unusually large number of + quads in one frame. +- **Blob and ImageData texture sources now get a cache key.** Two textures from + the same source object share one entry, so mutating a Blob in place and + re-creating the texture no longer forces a fresh upload. + +## Build defines + +Three defines are gone and should be deleted — `__RTT__`, `__enableAutosize__` +and `__calculateFps__` — along with `__dirtyQuadBuffer__`, removed in 1.8.3. +Make the five surviving flags explicit; an undefined flag leaves a runtime +`typeof` check the bundler cannot fold, so the guarded branch and its imports +stay in the bundle. + +```js +// vite.config.js +define: { + __DEV__: false, + __enableInspector__: false, + __emitBoundsEvents__: false, // true if you listen to viewport events + __enableCompressedTextures__: false, // true only if you ship .ktx/.pvr + __renderTextBatching__: true, +} +``` + +See [SolidTV Renderer](/articles/solidtv_renderer.md) for what each one guards. + +## Telemetry + +Only relevant if your app subscribes to `fpsUpdate`, `frameTick` or +`renderUpdate` — see the [FPS Counter](/primitives/fpscounter.md) page for the +full payload notes. In short: + +- `fpsUpdateInterval` is now the single switch for frame sampling, `renderUpdate` + included, and it is honored in production builds. +- `fps` measures rendering only and is `0` over an entirely idle interval, so a + dashboard alerting on "fps below N" will fire on a quiet screen. Read + `idleTicks` alongside it. +- `capabilities` left the payload; call `renderer.getCapabilities()` once at + startup. +- `frameTick.time` is `performance.now()`, measured from page load rather than + the Unix epoch. + +FPS series recorded from production builds before 1.8 are not comparable with +1.8+ numbers. + +## Frame rate cap + +`targetFPS` now defaults to `60` when left undefined. On TV targets an uncapped +loop draws every catch-up rAF the browser fires under GPU load, which measured +around 140fps on a 60Hz panel. Set `targetFPS: 0` to run uncapped. + +## What to check after upgrading + +- **Scrolling rows**: nodes appear with textures already loaded, nothing pops in + late or renders blank at the edges. +- **Every container that had `autosize`**: it still has a size, still clips, and + its children are positioned. +- **Anything that used `rtt`**: whatever replacement you settled on. +- **Viewport listeners**: entry and exit both fire, and `__emitBoundsEvents__` is + `true`. +- **Text**: letter spacing and font style measurement were fixed in 1.8, so + spaced text is measured the way it is drawn. Expect small layout shifts in text + using `letterSpacing`, and treat them as corrections. diff --git a/docs/articles/solidtv_renderer.md b/docs/articles/solidtv_renderer.md index a299b97e..2de11385 100644 --- a/docs/articles/solidtv_renderer.md +++ b/docs/articles/solidtv_renderer.md @@ -13,7 +13,7 @@ npm install @solidtv/renderer Or edit your package.json with: ```json -"@solidtv/renderer": "npm:@solidtv/renderer@3.2.5", +"@solidtv/renderer": "^1.9.0", ``` ## Configuration (Vite Defines) @@ -21,41 +21,39 @@ Or edit your package.json with: The SolidTV Renderer exposes several flags that can be configured via Vite defines in your `vite.config.ts`. These allow you to fine-tune the renderer's behavior for development, debugging, and production environments. ```js +// vite.config.ts define: { __DEV__: mode !== 'production', - __RTT__: true, - __renderTextBatching__: true, - __enableAutosize__: false, + __enableInspector__: mode !== 'production', __enableCompressedTextures__: false, - __calculateFps__: mode !== 'production', - __dirtyQuadBuffer__: true, + __renderTextBatching__: true, __emitBoundsEvents__: false, }, ``` -### `__DEV__` - -**Type:** `boolean` | **Default:** `undefined` (resolves `isProductionEnvironment` to `true`) - -Toggles development mode. When set to `true`, it enables development-specific features, warnings, and unoptimized paths useful for debugging. In production, this should be `false` or left undefined. - -### `__RTT__` +**Leaving a flag undefined is not the same as setting it to its default.** An +undefined global leaves a runtime `typeof` check that no bundler can fold, so the +guarded branch — and every module it imports — stays in the bundle even when the +flag's default is `false`. Defining all five is worth roughly 11.6 kB minified +(3.7 kB gzipped) on a WebGL build, most of it from `__enableCompressedTextures__` +dropping the PVR/KTX/ASTC parser. -**Type:** `boolean` | **Default:** `true` - -Enables or disables Render To Texture (RTT). This allows offscreen rendering of complex component trees into a static texture, which can significantly improve rendering performance for static content. +> Note for esbuild users: esbuild does not do the cross-module constant +> propagation this relies on, so the constants fold but the dead branches and +> their imports remain. Rollup, and therefore Vite, does strip them. -### `__renderTextBatching__` +### `__DEV__` -**Type:** `boolean` | **Default:** `true` +**Type:** `boolean` | **Default:** `false` -Enables batching for text rendering. When enabled, the renderer batches text draw calls together, reducing overall overhead and improving text-heavy application performance. This will place Text on top of other elements. If you find Text over an element that should be on top of Text, add a zIndex to create a new layer for text. +Toggles development mode. When set to `true`, it enables development-specific features, warnings, and unoptimized paths useful for debugging. In production, this should be `false`. -### `__enableAutosize__` +### `__enableInspector__` -**Type:** `boolean` | **Default:** `false` +**Type:** `boolean` | **Default:** `!isProductionEnvironment` -Enables automatic size calculations for elements based on their content. Turning this on can have a performance cost, so it defaults to `false`. Images will still work with autosize. This shouldn't be needed as SolidTV has flex which can calculate sizes. +Enables the DOM inspector hooks in the renderer. Pair it with the `inspector` +renderer option to actually mount the inspector. ### `__enableCompressedTextures__` @@ -63,20 +61,34 @@ Enables automatic size calculations for elements based on their content. Turning Enables support for compressed texture formats. Using compressed textures can significantly reduce memory usage and improve loading times, especially on constrained devices. However most folks are not using compressed textures, so this is disabled by default. ktx, pvr are the two supported formats for compressed textures. -### `__calculateFps__` - -**Type:** `boolean` | **Default:** `!isProductionEnvironment` - -Calculates and exposes frames per second (FPS) metrics. By default, it is active in development mode (`__DEV__ = true`) but disabled in production to avoid unnecessary overhead. +Turning it on is only half of what a `.ktx`/`.pvr` build needs — neither +extension is in Vite's default asset list, so add `assetsInclude: ['**/*.pvr', '**/*.ktx']` +and import the asset so you get an emitted URL back. -### `__dirtyQuadBuffer__` +### `__renderTextBatching__` **Type:** `boolean` | **Default:** `true` -An optimization technique that maintains a dirty quad buffer to reduce unnecessary recalculations during rendering updates. +Enables batching for text rendering. When enabled, the renderer batches text draw calls together, reducing overall overhead and improving text-heavy application performance. This will place Text on top of other elements. If you find Text over an element that should be on top of Text, add a zIndex to create a new layer for text. + +This is a performance feature that defaults on. Define it as `true` to fold the +branch, not to turn anything off. ### `__emitBoundsEvents__` **Type:** `boolean` | **Default:** `false` -When enabled, the renderer will emit events whenever an element's bounding box recalculates or changes. This can be useful for advanced layout tracking or debugging but may add overhead if heavily utilized. +Gates the `inViewport` / `outOfViewport` node events. Set it to `true` if your app +listens to them — see [Events](/essentials/events.md). + +## Removed flags + +These were removed in the 1.8 / 1.9 releases. Defining them has no effect, and +they should be deleted from your bundler config. + +| Flag | Removed in | Notes | +| --------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------ | +| `__RTT__` | 1.8.0 | Render-to-texture was removed in full, along with the `rtt` prop and the `RenderTexture` texture type. No replacement. | +| `__enableAutosize__` | 1.8.0 | `autosize` is now texture-only and always on for that case. It no longer sizes a container to its children. | +| `__dirtyQuadBuffer__` | 1.8.3 | The quad buffer is now rebuilt and uploaded in full every frame; the surgical upload path it gated is gone. | +| `__calculateFps__` | 1.9.0 | `fpsUpdateInterval` is the single switch for frame telemetry (`renderUpdate` included), and it is honored in production. | diff --git a/docs/essentials/events.md b/docs/essentials/events.md index 14c6352f..25aa9e0e 100644 --- a/docs/essentials/events.md +++ b/docs/essentials/events.md @@ -34,12 +34,6 @@ In addition to the lifecycle events from SolidTV, the SolidTV Renderer offers ad failed: (element, eventInfo) => { console.log('fail was called'); }, - inBounds: (element, eventInfo) => { - console.log('Element entered bounds'); - }, - outOfBounds: (element, eventInfo) => { - console.log('Element exited bounds'); - }, inViewport: (element, eventInfo) => { console.log('Element entered viewport'); }, @@ -55,11 +49,22 @@ In addition to the lifecycle events from SolidTV, the SolidTV Renderer offers ad - **`loaded`**: Fired when the element has successfully loaded. - **`failed`**: Fired when the element fails to load. - **`freed`**: Fired when the element is freed for memory. -- **`inBounds`**: Fired when the element enters the bounds of the visible screen area. -- **`outOfBounds`**: Fired when the element leaves the visible screen area. - **`inViewport`**: Fired when the element enters the viewport (the portion of the screen where content is visible). - **`outOfViewport`**: Fired when the element leaves the viewport. +Both viewport events receive a `{ previous, current }` payload of +`CoreNodeRenderState` values. + +> **Renderer 1.8 breaking change:** `inBounds` and `outOfBounds` were removed. +> They fired on the preload transition, one step earlier than viewport entry. +> Preloading textures ahead of a node scrolling on screen is now handled by the +> `boundsMargin` renderer setting, so there is nothing left for an app to do on +> that transition — move any such listener to `inViewport`. +> +> Viewport events are gated behind the `__emitBoundsEvents__` build flag, which +> defaults to `false`. See [SolidTV Renderer](/articles/solidtv_renderer.md) to +> turn it on. + These additional events provide control over element state and position within the SolidTV Renderer, allowing you to react to changes such as visibility or load state with custom logic. ## Emitting Custom Events with `emit` diff --git a/docs/essentials/images.md b/docs/essentials/images.md index 34bec219..64fac22a 100644 --- a/docs/essentials/images.md +++ b/docs/essentials/images.md @@ -14,6 +14,10 @@ Just give any `` tag a src to an image. Be sure to also give it a width an The SolidTV renderer will scale the image to fit the width and height dimensions provided. If you don't know the size of the image you can use `autosize` attribute for the Renderer to set the image size when it loads. +Since renderer 1.8 `autosize` means only that: take the texture's intrinsic +dimensions once it loads. It does not size a container to its children (use flex +for that), and it is a no-op on `` nodes. + For the best performance, it's important to keep your source images as small as possible. If you're displaying an image at `200px x 200px`, make sure the image is exactly that size or _smaller_. The latter option may lead to some quality loss, but can positively impact the overall performance of your App. ## textureOptions for images diff --git a/docs/essentials/render.md b/docs/essentials/render.md index d5bd6c24..b50ce13b 100644 --- a/docs/essentials/render.md +++ b/docs/essentials/render.md @@ -40,7 +40,6 @@ Config.rendererOptions = { // textureMemory: { // criticalThreshold: 80e6, // }, - numImageWorkers, // temp fix for renderer bug // Set the resolution based on window height // 720p = 0.666667, 1080p = 1, 1440p = 1.5, 2160p = 2 deviceLogicalPixelRatio: 1, @@ -61,7 +60,9 @@ For the latest renderer options read the official [renderer documentation](https - **txMemByteThreshold**: Texture Memory Byte Threshold. When the GPU VRAM used by textures exceeds this threshold, non-visible textures are freed. Set to `0` to disable. -- **boundsMargin**: Bounds margin to extend the boundary for adding a CoreNode as Quad. Can be a single number or an array of four numbers. +- **boundsMargin**: Preload margin, in logical pixels, around the viewport. Its job is to load textures ahead of a node scrolling into view. A single number applied to all sides. + - _Default_: `200` + - The `[top, right, bottom, left]` array form was dropped in renderer 1.8. It is still tolerated (the largest edge wins, with a console warning) but should be replaced with a single number. - **deviceLogicalPixelRatio**: Factor to convert app-authored logical coordinates to device logical coordinates. Supports auto-scaling for different resolutions. - _Default_: `1` @@ -75,14 +76,23 @@ For the latest renderer options read the official [renderer documentation](https - **Texture Memory Manager Settings**: textureMemory?: Partial; -- **fpsUpdateInterval**: Interval in milliseconds for receiving FPS updates. Set to `0` to disable. +- **fpsUpdateInterval**: Sampling interval in milliseconds for the `fpsUpdate` and `renderUpdate` events. Set to `0` to disable. - _Default_: `0` + - Since 1.9 this is the single switch for frame telemetry and it is honored in production builds. The `__calculateFps__` build flag is gone. + +- **targetFPS**: Caps the render loop. `0` runs uncapped at the display refresh rate. + - _Default_: `60` + - Left undefined, the loop caps at 60. On TV targets an uncapped loop draws every catch-up rAF the browser fires under GPU load (measured ~140fps on a 60Hz panel). + +- **textLayoutCacheSize**: Maximum number of entries kept in the SDF text layout cache. + - _Default_: `250` - **enableContextSpy**: Includes WebGL context call information in FPS updates. Significantly impacts performance. - _Default_: `false` -- **numImageWorkers**: Number of image workers to use. Improves image loading on multi-core devices. Set to `0` to disable. - - _Default_: `2` +- **numImageWorkers**: Number of image workers to use. Set to `0` to keep image loading on the main thread. + - _Default_: `1` + - Clamped to at most `1` since renderer 1.8: the pool measured 99.7% idle, and a second worker raised neither throughput nor images in flight. Passing `2` or `4` silently yields `1`. - **inspector** Optional. Allows inspection of the state of Nodes in the renderer, replicating the node state. @@ -94,12 +104,16 @@ For the latest renderer options read the official [renderer documentation](https - **quadBufferSize** Specifies the quad buffer size in bytes. - Default: `4 * 1024 * 1024`. + Default: `1048576` (16384 quads x 64 bytes — the most a Uint16 index buffer can address). Was `1310720` before renderer 1.8. - **fontEngines** Defines font engines for text rendering (CanvasTextRenderer for Canvas, SdfTextRenderer for WebGL). Enables tree shaking for unused engines. Default: `[]`. Type: `(typeof SdfTextRenderer | typeof CanvasTextRenderer)[]`. +#### Removed renderer settings + +- **renderOnlyInViewport**: removed in renderer 1.8. Its `true` behavior is now unconditional — the renderer always draws only what is in view. + ### Additional Solid-Specific Configurations Besides `rendererOptions`, the `Config` object exposes several properties specific to `@solidtv/solid` runtime behavior: diff --git a/docs/essentials/styling.md b/docs/essentials/styling.md index 34dd4657..87630206 100644 --- a/docs/essentials/styling.md +++ b/docs/essentials/styling.md @@ -149,7 +149,7 @@ These are found in the Renderer and applicable to all nodes: - `width`: The width of the Node, default is `0`. - `height`: The height of the Node, default is `0`. - `alpha`: The alpha opacity of the Node, ranging from `0` (transparent) to `1` (opaque), default is `1`. -- `autosize`: When enabled, the Node resizes to the dimensions of its texture, default is `false`. +- `autosize`: When enabled, the Node resizes to the intrinsic dimensions of its texture once that texture loads, default is `false`. It only applies to nodes with a texture (`src`) — it does not size a container to its children, and it is a no-op on `` nodes. Use flex to size containers. - `clipping`: Prevents drawing outside the Node's bounds, default is `false`. - `color`: The color of the Node in 0xRRGGBBAA format, default is `0xffffffff` (opaque white). - `colorTop`: The color of the Node's top edge for gradient rendering. @@ -176,7 +176,11 @@ These are found in the Renderer and applicable to all nodes: - `pivotX`: X position of the Node's Pivot Point, default is `0.5`. - `pivotY`: Y position of the Node's Pivot Point, default is `0.5`. - `rotation`: Rotation of the Node in radians. -- `rtt`: Whether the Node is rendered to a texture, default is `false`. + +> **Renderer 1.8 breaking change:** the `rtt` (render-to-texture) prop and the +> per-node `boundsMargin` prop were removed. There is no replacement for `rtt`; +> flatten the subtree into a pre-rendered image if you need the effect. +> `boundsMargin` is now a stage-wide renderer setting only. ### SDF Text Nodes diff --git a/docs/primitives/createTag.md b/docs/primitives/createTag.md deleted file mode 100644 index 5e288a0d..00000000 --- a/docs/primitives/createTag.md +++ /dev/null @@ -1,69 +0,0 @@ -# createTag - -`createTag` is a primitive that allows you to render a SolidTV node structure into a texture using the RTT (Render To Texture) feature. This is useful for creating complex UI elements that are static or updated infrequently, improving performance by reducing the number of active nodes in the render tree. The resulting tag can be used as a component multiple times. - -## Usage - -```tsx -import { createTag } from '@solidtv/solid'; -import { onCleanup } from 'solid-js'; - -const App = () => { - const DramaTag = createTag( - - Drama - , - ); - - const NewEpisodeTag = createTag( - - - New Episode - - , - ); - - onCleanup(() => { - DramaTag.destroy(); - NewEpisodeTag.destroy(); - }); - - return ( - - - - - ); -}; -``` - -## API - -### `createTag(children: JSX.Element): Component & { destroy: () => void }` - -Creates a tag component from the provided children. - -- **Parameters**: - - `children`: The SolidTV/SolidTV elements to render into the texture. - -- **Returns**: - - A SolidTV component that renders the generated texture. - - The component has a static `destroy()` method. - -### `TagComponent.destroy()` - -Frees the texture memory associated with the tag. It is important to call this when the tag is no longer needed to prevent memory leaks, typically inside an `onCleanup` block or when the parent component unmounts. diff --git a/docs/primitives/fpscounter.md b/docs/primitives/fpscounter.md index 6128f2a7..3a718283 100644 --- a/docs/primitives/fpscounter.md +++ b/docs/primitives/fpscounter.md @@ -4,6 +4,10 @@ This component displays the current frames per second (FPS) of the application. To use, import FPSCounter and add it to your component tree. import { setupFPS } from '@solidtv/solid'; On your canvas element add renderer option: fpsUpdateInterval: 200 and ref={(root) => setupFPS(root)} +`fpsUpdateInterval` is required — since renderer 1.9 it is the single switch for +both the `fpsUpdate` and `renderUpdate` events, and it works in production +builds. The `__calculateFps__` build flag that used to gate them is gone. + ```jsx import { FPSCounter, setupFPS } from '@solidtv/solid/primitives'; import { renderer } from '@solidtv/solid'; @@ -14,3 +18,26 @@ setupFPS({ renderer }); ; ``` + +## Reading the numbers + +Renderer 1.8 reworked the `fpsUpdate` payload: + +- `fps` now measures **rendering only** and is `0` when the interval was + entirely idle, so a quiet screen reads as 0 rather than as a stall. Read + `idleTicks` alongside it to tell the two apart. `FPSCounter` ignores samples + at or below 5 fps for this reason. +- `capabilities` was removed from the payload. Call `renderer.getCapabilities()` + once at startup instead; the `RendererCapabilities` type is exported from + `@solidtv/renderer`. +- `frameTick.time` now comes from `performance.now()`, measured from page load + rather than the Unix epoch. +- New fields worth having: `animatedFps` (the rate over frames where something + was actually moving), `frameTimeBuckets` and `maxFrameTime` for percentiles, + and `updateMs` / `renderMs` / `uploadMs` to attribute a regression to a phase. + `renderer.setTelemetrySegment('home')` labels a sample so an interval never + spans two screens. + +FPS series recorded from production builds before renderer 1.8 are not +comparable with these numbers: production builds used to sample the idle poll +cadence and report a meaningless constant around 60-70. diff --git a/package.json b/package.json index 68ac4d38..856e905f 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ }, "peerDependencies": { "@solidjs/router": "^0.16.1", - "@solidtv/renderer": "^1.6.3", + "@solidtv/renderer": "^1.9.0", "solid-js": "*" }, "peerDependenciesMeta": {}, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d301270..17e6c080 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^0.16.1 version: 0.16.1(solid-js@1.9.3) '@solidtv/renderer': - specifier: ^1.6.3 - version: 1.6.3 + specifier: ^1.9.0 + version: 1.9.0 solid-js: specifier: '*' version: 1.9.3 @@ -869,8 +869,8 @@ packages: '@solidjs/router': optional: true - '@solidtv/renderer@1.6.3': - resolution: {integrity: sha512-GRm92jZtWQ5ltWlVLZ02YjFjNeaweGoXCl3tzZyw6FTHPEMwERbD/+gZVfx0xiCDmq2GsqmFpgCRY3wuMc0sQQ==} + '@solidtv/renderer@1.9.0': + resolution: {integrity: sha512-61MLroT4fPGpslSIiBpQHAmx017iyMfZhVRc0OQalsBGyGng6lH0SkWwkIX4W9l5umqVH30pzIcjpn5+2OQZzg==} engines: {node: '>= 18.0.0', npm: '>= 10.0.0', pnpm: '>= 10.17.0'} '@standard-schema/spec@1.1.0': @@ -3197,7 +3197,7 @@ snapshots: optionalDependencies: '@solidjs/router': 0.16.1(solid-js@1.9.3) - '@solidtv/renderer@1.6.3': {} + '@solidtv/renderer@1.9.0': {} '@standard-schema/spec@1.1.0': {} diff --git a/src/core/config.ts b/src/core/config.ts index 2b6e0558..4970f0b4 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -54,7 +54,13 @@ export interface Config { animationSettings?: AnimationSettings; animationsEnabled: boolean; fontSettings: Partial; - rendererOptions?: Partial | DomRendererMainSettings; + /** + * Renderer settings. Typed as an intersection rather than a union so a single + * config object can be authored without narrowing per renderer: which fields + * are honored depends on the active renderer (WebGL/Canvas vs DOM), and every + * field is optional. + */ + rendererOptions?: Partial & DomRendererMainSettings; /** * Hook the focus manager calls to publish the active element. Defaults to * writing the {@link activeElement} signal directly; a custom focus manager diff --git a/src/core/dom-renderer/domRenderer.ts b/src/core/dom-renderer/domRenderer.ts index 8b089a73..818fc6e4 100644 --- a/src/core/dom-renderer/domRenderer.ts +++ b/src/core/dom-renderer/domRenderer.ts @@ -488,7 +488,9 @@ function updateNodeStyles(node: DOMNode | DOMText) { >; srcPos = texture.props; rawImgSrc = (texture.props.texture as any).props.src; - } else if (props.src) { + } else if (typeof props.src === 'string') { + // Renderer 1.8 widened `src` to `string | Blob | ImageData`; the DOM + // renderer only paints string URLs. rawImgSrc = props.src; } @@ -1256,7 +1258,6 @@ function resolveNodeDefaults( alpha: props.alpha ?? 1, ignoreParentAlpha: props.ignoreParentAlpha ?? false, autosize: props.autosize ?? false, - boundsMargin: props.boundsMargin ?? null, clipping: props.clipping ?? false, color, colorTop: props.colorTop ?? color, @@ -1289,7 +1290,6 @@ function resolveNodeDefaults( pivotX: props.pivotX ?? props.pivot ?? 0.5, pivotY: props.pivotY ?? props.pivot ?? 0.5, rotation: props.rotation ?? 0, - rtt: props.rtt ?? false, placeholderColor: props.placeholderColor ?? 0, data: {}, imageType: props.imageType, @@ -1329,6 +1329,7 @@ const defaultShader: IRendererShader = { let lastNodeId = 0; +/** Render-state labels, used for the `data-state` debug attribute only. */ const CoreNodeRenderStateMap = new Map([ [0, 'init'], [2, 'outOfBounds'], @@ -1436,15 +1437,20 @@ export class DOMNode extends EventEmitter implements IRendererNode { if (renderState === this.renderState) return; const previous = this.renderState; this.renderState = renderState; - const event = CoreNodeRenderStateMap.get(renderState); if (isRenderStateInBounds(renderState)) { this.applyPendingImageSrc(); } - if (event && event !== 'init') { - this.emit(event, { previous, current: renderState }); + // Viewport entry/exit is the whole observable surface, matching CoreNode. + // `inBounds` stays internal: it only exists so textures preload ahead of a + // node scrolling on screen. Exit fires for InViewport -> InBounds and + // InViewport -> OutOfBounds alike. + if (renderState === 8 /* InViewport */) { + this.emit('inViewport', { previous, current: renderState }); + } else if (previous === 8 /* InViewport */) { + this.emit('outOfViewport', { previous, current: renderState }); } if (this.imgEl) { - this.imgEl.dataset.state = event; + this.imgEl.dataset.state = CoreNodeRenderStateMap.get(renderState); } } @@ -1763,13 +1769,6 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.markChildrenBoundsDirty(); updateTransformOnly(this); } - get rtt() { - return this.props.rtt; - } - set rtt(v) { - this.props.rtt = v; - updateNodeStyles(this); - } get shader() { return this.props.shader; } @@ -1817,15 +1816,6 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.props.srcY = v; } - get boundsMargin(): number | [number, number, number, number] | null { - return this.props.boundsMargin; - } - set boundsMargin(value: number | [number, number, number, number] | null) { - this.props.boundsMargin = value; - this.boundsDirty = true; - this.markChildrenBoundsDirty(); - } - get ignoreParentAlpha(): boolean { return this.props.ignoreParentAlpha; } @@ -2279,9 +2269,6 @@ export class DOMRendererMain implements IRendererMain { case 'NoiseTexture': type = lng.TextureType.noise; break; - case 'RenderTexture': - type = lng.TextureType.renderToTexture; - break; } return { type, props } as InstanceType; } diff --git a/src/core/dom-renderer/domRendererTypes.ts b/src/core/dom-renderer/domRendererTypes.ts index 73cf833e..8565ea5e 100644 --- a/src/core/dom-renderer/domRendererTypes.ts +++ b/src/core/dom-renderer/domRendererTypes.ts @@ -12,7 +12,7 @@ import { /** Based on {@link lng.CoreRenderer} */ export interface IRendererCoreRenderer { mode: 'canvas' | 'webgl' | undefined; - boundsMargin?: number | [number, number, number, number]; + boundsMargin?: number; } /** Based on {@link lng.TrFontManager} */ export interface IRendererFontManager { @@ -145,8 +145,12 @@ export interface DomRendererMainSettings { deviceLogicalPixelRatio?: number; /** - * Bounds margin for the renderer - * Can be a single number (applied to all sides) or an array [top, right, bottom, left] + * Preload margin around the viewport, in logical pixels (default: 200) + * + * @remarks + * A single number applied to all sides. The `[top, right, bottom, left]` + * array form was dropped in renderer 1.8; it is still tolerated at runtime + * (the largest edge wins, with a warning) but should be replaced. */ - boundsMargin?: number | [number, number, number, number]; + boundsMargin?: number; } diff --git a/src/core/dom-renderer/domRendererUtils.ts b/src/core/dom-renderer/domRendererUtils.ts index fe6fc875..07ca5d45 100644 --- a/src/core/dom-renderer/domRendererUtils.ts +++ b/src/core/dom-renderer/domRendererUtils.ts @@ -221,17 +221,31 @@ export function nodeHasTextureSource(node: DOMNode): boolean { ); } +/** + * Coerce a `boundsMargin` value to the scalar the renderer takes since 1.8. + * + * @remarks + * The `[top, right, bottom, left]` array form was dropped in renderer 1.8. An + * untyped app upgrading from 1.7 can still pass it, so mirror the WebGL + * renderer and take the widest edge rather than letting the margin arithmetic + * go wrong. + */ export function normalizeBoundsMargin( - margin: number | [number, number, number, number] | null | undefined, -): [number, number, number, number] { - if (margin == null) return [0, 0, 0, 0]; - if (typeof margin === 'number') { - return [margin, margin, margin, margin]; - } - if (Array.isArray(margin) && margin.length === 4) { - return [margin[0] ?? 0, margin[1] ?? 0, margin[2] ?? 0, margin[3] ?? 0]; + margin: number | number[] | null | undefined, +): number { + if (margin == null) return 0; + if (Array.isArray(margin) === false) return margin as number; + const arr = margin as number[]; + let max = 0; + for (let i = 0; i < arr.length; i++) { + if (arr[i]! > max) { + max = arr[i]!; + } } - return [0, 0, 0, 0]; + console.warn( + `boundsMargin array form is no longer supported, using the largest edge value: ${max}`, + ); + return max; } export function computeRenderStateForNode( @@ -249,10 +263,7 @@ export function computeRenderStateForNode( const rootRight = rootLeft + rootWidth; const rootBottom = rootTop + rootHeight; - const [marginTop, marginRight, marginBottom, marginLeft] = - normalizeBoundsMargin( - node.props.boundsMargin ?? node.stage.renderer.boundsMargin, - ); + const margin = normalizeBoundsMargin(node.stage.renderer.boundsMargin); const width = node.props.w ?? 0; const height = node.props.h ?? 0; @@ -262,10 +273,10 @@ export function computeRenderStateForNode( const right = left + width; const bottom = top + height; - const expandedLeft = rootLeft - marginLeft; - const expandedTop = rootTop - marginTop; - const expandedRight = rootRight + marginRight; - const expandedBottom = rootBottom + marginBottom; + const expandedLeft = rootLeft - margin; + const expandedTop = rootTop - margin; + const expandedRight = rootRight + margin; + const expandedBottom = rootBottom + margin; const intersectsBounds = right >= expandedLeft && diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 95ff4074..433e898b 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -277,7 +277,6 @@ const LightningRendererNonAnimatingProps = [ 'overflowSuffix', 'placeholderColor', 'preventCleanup', - 'rtt', 'scrollable', 'scrollY', 'srcHeight', @@ -743,12 +742,10 @@ export interface ElementNode extends RendererNode, FocusNode { * - 'loaded' * - 'failed' * - 'freed' - * - 'inBounds' - * - 'outOfBounds' * - 'inViewport' * - 'outOfViewport' * - * @typedef {'loaded' | 'failed' | 'freed' | 'inBounds' | 'outOfBounds' | 'inViewport' | 'outOfViewport'} NodeEvents + * @typedef {'loaded' | 'failed' | 'freed' | 'inViewport' | 'outOfViewport'} NodeEvents * * @param {Partial>} events - An object where the keys are event names from NodeEvents and the values are the respective event handlers. * @returns {void} @@ -875,11 +872,7 @@ export class ElementNode { set id(id: string) { this._id = id; - if ( - Config.rendererOptions && - 'inspector' in Config.rendererOptions && - Config.rendererOptions.inspector - ) { + if (Config.rendererOptions?.inspector) { this.data = { ...this.data, testId: id }; } } @@ -1256,7 +1249,10 @@ export class ElementNode { } get src(): string | null | undefined { - return this.lng.src; + // Renderer 1.8 widened `src` to `string | Blob | ImageData`. The solid + // `src` prop only ever assigns strings (see the setter above), so the + // non-string arms are unreachable through this path. + return this.lng.src as string | null | undefined; } getChildById(id: string) { @@ -1701,10 +1697,6 @@ export class ElementNode { node._calcHeight = true; } - if (props.rtt && !props.color) { - props.color = 0xffffffff; - } - if (!props.color && !props.src) { // Default color to transparent - If you later set a src, you'll need // to set color '#ffffffff' diff --git a/src/core/intrinsicTypes.ts b/src/core/intrinsicTypes.ts index 4170ff04..5d8d9c13 100644 --- a/src/core/intrinsicTypes.ts +++ b/src/core/intrinsicTypes.ts @@ -200,10 +200,8 @@ type EventPayloadMap = { loaded: lngr.NodeLoadedPayload; failed: lngr.NodeFailedPayload; freed: Event; - inBounds: Event; - outOfBounds: Event; - inViewport: Event; - outOfViewport: Event; + inViewport: lngr.NodeViewportPayload; + outOfViewport: lngr.NodeViewportPayload; }; type NodeEvents = keyof EventPayloadMap; diff --git a/src/primitives/FadeInOut.tsx b/src/primitives/FadeInOut.tsx index 55102beb..d69a321a 100644 --- a/src/primitives/FadeInOut.tsx +++ b/src/primitives/FadeInOut.tsx @@ -40,7 +40,6 @@ export function FadeInOut(props: Props & NodeProps) { } function onDestroy(elm: ElementNode) { - elm.rtt = true; return elm .animate( { alpha: 0 }, diff --git a/src/primitives/Marquee.tsx b/src/primitives/Marquee.tsx index f6595650..fc7d0b4f 100644 --- a/src/primitives/Marquee.tsx +++ b/src/primitives/Marquee.tsx @@ -96,8 +96,8 @@ export function MarqueeText(props: MarqueeTextProps) { return ( <> {wasFocusedBefore() && <> -