From f7c25bda18f869081e003814f57806cf2cac233e Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 4 Dec 2025 14:55:10 +0100 Subject: [PATCH 1/2] :wrench: --- packages/skia/src/sksg/HostConfig.ts | 8 +- packages/skia/src/sksg/Node.ts | 127 +++-- .../src/sksg/Recorder/ReanimatedRecorder.ts | 5 +- packages/skia/src/sksg/Recorder/Recorder.ts | 9 +- packages/skia/src/sksg/Recorder/Visitor.ts | 441 +++++++++--------- 5 files changed, 336 insertions(+), 254 deletions(-) diff --git a/packages/skia/src/sksg/HostConfig.ts b/packages/skia/src/sksg/HostConfig.ts index 5fc75b8816..331f86b0a4 100644 --- a/packages/skia/src/sksg/HostConfig.ts +++ b/packages/skia/src/sksg/HostConfig.ts @@ -7,6 +7,7 @@ import type { NodeType } from "../dom/types"; import { shallowEq } from "../renderer/typeddash"; import type { Node } from "./Node"; +import { bumpChildrenVersion } from "./Node"; import type { Container } from "./StaticContainer"; type EventPriority = number; @@ -97,16 +98,18 @@ export const sksgHostConfig: SkiaHostConfig = { // eslint-disable-next-line @typescript-eslint/no-explicit-any const { children, ...props } = propsWithChildren as any; debug("createInstance", type); - const instance = { + const instance: Node = { type, props, children: [], + __skChildrenVersion: 0, }; return instance; }, appendInitialChild(parentInstance: Instance, child: Instance | TextInstance) { parentInstance.children.push(child); + bumpChildrenVersion(parentInstance); }, finalizeInitialChildren( @@ -185,6 +188,9 @@ export const sksgHostConfig: SkiaHostConfig = { type: instance.type, props: { ...newProps }, children: keepChildren ? [...instance.children] : [], + __skChildrenVersion: keepChildren + ? instance.__skChildrenVersion ?? 0 + : 0, }; }, diff --git a/packages/skia/src/sksg/Node.ts b/packages/skia/src/sksg/Node.ts index 1ec06ad7c4..4a04ca3546 100644 --- a/packages/skia/src/sksg/Node.ts +++ b/packages/skia/src/sksg/Node.ts @@ -4,8 +4,53 @@ export interface Node { type: NodeType; props: Props; children: Node[]; + __skChildrenVersion?: number; + __skSortedChildren?: SortedChildrenCache; } +export interface SortedChildren { + colorFilters: Node[]; + maskFilters: Node[]; + shaders: Node[]; + imageFilters: Node[]; + pathEffects: Node[]; + drawings: Node[]; + paints: Node[]; +} + +type SortedChildrenCache = { + version: number; + buckets: SortedChildren; +}; + +const createBuckets = (): SortedChildren => ({ + colorFilters: [], + maskFilters: [], + shaders: [], + imageFilters: [], + pathEffects: [], + drawings: [], + paints: [], +}); + +const resetBuckets = (buckets: SortedChildren) => { + buckets.colorFilters.length = 0; + buckets.maskFilters.length = 0; + buckets.shaders.length = 0; + buckets.imageFilters.length = 0; + buckets.pathEffects.length = 0; + buckets.drawings.length = 0; + buckets.paints.length = 0; +}; + +export const bumpChildrenVersion = (node: Node) => { + node.__skChildrenVersion = (node.__skChildrenVersion ?? 0) + 1; +}; + +export const getChildrenVersion = (node: Node) => { + return node.__skChildrenVersion ?? 0; +}; + export const isColorFilter = (type: NodeType) => { "worklet"; return ( @@ -60,47 +105,59 @@ export const isShader = (type: NodeType) => { ); }; -export const sortNodeChildren = (parent: Node) => { +export const sortNodeChildren = (parent: Node): SortedChildren => { "worklet"; - const maskFilters: Node[] = []; - const colorFilters: Node[] = []; - const shaders: Node[] = []; - const imageFilters: Node[] = []; - const pathEffects: Node[] = []; - const drawings: Node[] = []; - const paints: Node[] = []; - parent.children.forEach((node) => { - if (isColorFilter(node.type)) { - colorFilters.push(node); - } else if (node.type === NodeType.BlurMaskFilter) { - maskFilters.push(node); - } else if (isPathEffect(node.type)) { - pathEffects.push(node); - } else if (isImageFilter(node.type)) { - imageFilters.push(node); - } else if (isShader(node.type)) { - shaders.push(node); - } else if (node.type === NodeType.Paint) { - paints.push(node); - } else if (node.type === NodeType.Blend) { + const version = getChildrenVersion(parent); + let cache = parent.__skSortedChildren; + + // Return cached result if version matches + if (cache && cache.version === version) { + return cache.buckets; + } + + // Create or reset cache + if (!cache) { + cache = { + version, + buckets: createBuckets(), + }; + parent.__skSortedChildren = cache; + } else { + cache.version = version; + resetBuckets(cache.buckets); + } + + const buckets = cache.buckets; + const children = parent.children; + const len = children.length; + + for (let i = 0; i < len; i++) { + const node = children[i]; + const type = node.type; + if (isColorFilter(type)) { + buckets.colorFilters.push(node); + } else if (type === NodeType.BlurMaskFilter) { + buckets.maskFilters.push(node); + } else if (isPathEffect(type)) { + buckets.pathEffects.push(node); + } else if (isImageFilter(type)) { + buckets.imageFilters.push(node); + } else if (isShader(type)) { + buckets.shaders.push(node); + } else if (type === NodeType.Paint) { + buckets.paints.push(node); + } else if (type === NodeType.Blend) { if (node.children[0] && isImageFilter(node.children[0].type)) { node.type = NodeType.BlendImageFilter; - imageFilters.push(node); + buckets.imageFilters.push(node); } else { node.type = NodeType.Blend; - shaders.push(node); + buckets.shaders.push(node); } } else { - drawings.push(node); + buckets.drawings.push(node); } - }); - return { - colorFilters, - drawings, - maskFilters, - shaders, - pathEffects, - imageFilters, - paints, - }; + } + + return buckets; }; diff --git a/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts b/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts index c18d5e272a..b6ec45ba84 100644 --- a/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts +++ b/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts @@ -54,14 +54,15 @@ export class ReanimatedRecorder implements BaseRecorder { if (!props) { return; } - Object.values(props).forEach((value) => { + for (const key in props) { + const value = props[key]; if (isSharedValue(value) && !this.values.has(value)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error value.name = `variable${this.values.size}`; this.values.add(value as SharedValue); } - }); + } } getRecorder() { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 1a0b548791..a10a916b97 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -64,21 +64,22 @@ export class Recorder implements BaseRecorder { } private processProps(props: Record) { - const animatedProps: Record> = {}; - let hasAnimatedProps = false; + let animatedProps: Record> | undefined; for (const key in props) { const prop = props[key]; if (isSharedValue(prop)) { this.animationValues.add(prop); + if (!animatedProps) { + animatedProps = {}; + } animatedProps[key] = prop; - hasAnimatedProps = true; } } return { props, - animatedProps: hasAnimatedProps ? animatedProps : undefined, + animatedProps, }; } diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 2842b4b388..112d4e9324 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -10,175 +10,217 @@ import type { Node } from "../Node"; import { isImageFilter, isShader, sortNodeChildren } from "../Node"; import type { AnimatedProps } from "../../renderer"; -export const processPaint = ({ - opacity, - color, - strokeWidth, - blendMode, - style, - strokeJoin, - strokeCap, - strokeMiter, - antiAlias, - dither, - paint: paintRef, -}: DrawingNodeProps) => { - const paint: DrawingNodeProps = {}; - if (opacity !== undefined) { - paint.opacity = opacity; - } - if (color !== undefined) { - paint.color = color; - } - if (strokeWidth !== undefined) { - paint.strokeWidth = strokeWidth; - } - if (blendMode !== undefined) { - paint.blendMode = blendMode; - } - if (style !== undefined) { - paint.style = style; - } - if (strokeJoin !== undefined) { - paint.strokeJoin = strokeJoin; - } - if (strokeCap !== undefined) { - paint.strokeCap = strokeCap; - } - if (strokeMiter !== undefined) { - paint.strokeMiter = strokeMiter; - } - if (antiAlias !== undefined) { - paint.antiAlias = antiAlias; - } - if (dither !== undefined) { - paint.dither = dither; - } +// WeakMap caches for computed props - keyed by node, invalidated when props reference changes +type NodePropsCache = { + props: object; + value: T; +}; - if (paintRef !== undefined) { - paint.paint = paintRef; +const getCachedValue = ( + cache: WeakMap, NodePropsCache>, + node: Node, + props: object, + compute: () => T +): T => { + const cached = cache.get(node); + if (cached && cached.props === props) { + return cached.value; } + const value = compute(); + cache.set(node, { props, value }); + return value; +}; + +const paintCache = new WeakMap< + Node, + NodePropsCache +>(); +const ctmCache = new WeakMap, NodePropsCache>(); +const stackingContextCache = new WeakMap< + Node, + NodePropsCache> | undefined> +>(); + +export const processPaint = (props: DrawingNodeProps) => { + const { + opacity, + color, + strokeWidth, + blendMode, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, + paint: paintRef, + } = props; + // Early return if no paint properties are defined if ( - opacity !== undefined || - color !== undefined || - strokeWidth !== undefined || - blendMode !== undefined || - style !== undefined || - strokeJoin !== undefined || - strokeCap !== undefined || - strokeMiter !== undefined || - antiAlias !== undefined || - dither !== undefined || - paintRef !== undefined + opacity === undefined && + color === undefined && + strokeWidth === undefined && + blendMode === undefined && + style === undefined && + strokeJoin === undefined && + strokeCap === undefined && + strokeMiter === undefined && + antiAlias === undefined && + dither === undefined && + paintRef === undefined ) { - return paint; + return null; } - return null; + + const paint: DrawingNodeProps = {}; + if (opacity !== undefined) paint.opacity = opacity; + if (color !== undefined) paint.color = color; + if (strokeWidth !== undefined) paint.strokeWidth = strokeWidth; + if (blendMode !== undefined) paint.blendMode = blendMode; + if (style !== undefined) paint.style = style; + if (strokeJoin !== undefined) paint.strokeJoin = strokeJoin; + if (strokeCap !== undefined) paint.strokeCap = strokeCap; + if (strokeMiter !== undefined) paint.strokeMiter = strokeMiter; + if (antiAlias !== undefined) paint.antiAlias = antiAlias; + if (dither !== undefined) paint.dither = dither; + if (paintRef !== undefined) paint.paint = paintRef; + + return paint; }; -const processCTM = ({ - clip, - invertClip, - transform, - origin, - matrix, - layer, -}: CTMProps) => { - const ctm: CTMProps = {}; - if (clip) { - ctm.clip = clip; - } - if (invertClip) { - ctm.invertClip = invertClip; - } - if (transform) { - ctm.transform = transform; - } - if (origin) { - ctm.origin = origin; - } - if (matrix) { - ctm.matrix = matrix; - } - if (layer) { - ctm.layer = layer; - } +const processCTM = (props: CTMProps) => { + const { clip, invertClip, transform, origin, matrix, layer } = props; + + // Early return if no CTM properties are defined if ( - clip !== undefined || - invertClip !== undefined || - transform !== undefined || - origin !== undefined || - matrix !== undefined || - layer !== undefined + clip === undefined && + invertClip === undefined && + transform === undefined && + origin === undefined && + matrix === undefined && + layer === undefined ) { - return ctm; + return null; } - return null; + + const ctm: CTMProps = {}; + if (clip !== undefined) ctm.clip = clip; + if (invertClip !== undefined) ctm.invertClip = invertClip; + if (transform !== undefined) ctm.transform = transform; + if (origin !== undefined) ctm.origin = origin; + if (matrix !== undefined) ctm.matrix = matrix; + if (layer !== undefined) ctm.layer = layer; + + return ctm; +}; + +// Cached getters that use WeakMap + props reference check +const getPaintForNode = (node: Node): DrawingNodeProps | null => { + const propsObj = node.props as object; + return getCachedValue(paintCache, node, propsObj, () => + processPaint(node.props as DrawingNodeProps) + ); +}; + +const getCTMForNode = (node: Node): CTMProps | null => { + const propsObj = node.props as object; + return getCachedValue(ctmCache, node, propsObj, () => + processCTM(node.props as CTMProps) + ); +}; + +const computeStackingContextProps = ( + props: AnimatedProps +): AnimatedProps> | undefined => { + const { zIndex } = props; + if (zIndex === undefined) { + return undefined; + } + return { zIndex }; +}; + +const getStackingContextForNode = ( + node: Node +): AnimatedProps> | undefined => { + const propsObj = node.props as object; + return getCachedValue(stackingContextCache, node, propsObj, () => + computeStackingContextProps(node.props as AnimatedProps) + ); }; const pushColorFilters = ( recorder: BaseRecorder, colorFilters: Node[] ) => { - colorFilters.forEach((colorFilter) => { - if (colorFilter.children.length > 0) { + const len = colorFilters.length; + for (let i = 0; i < len; i++) { + const colorFilter = colorFilters[i]; + const childrenLen = colorFilter.children.length; + if (childrenLen > 0) { pushColorFilters(recorder, colorFilter.children); } recorder.pushColorFilter(colorFilter.type, colorFilter.props); const needsComposition = - colorFilter.type !== NodeType.LerpColorFilter && - colorFilter.children.length > 0; + colorFilter.type !== NodeType.LerpColorFilter && childrenLen > 0; if (needsComposition) { recorder.composeColorFilter(); } - }); + } }; const pushPathEffects = (recorder: BaseRecorder, pathEffects: Node[]) => { - pathEffects.forEach((pathEffect) => { - if (pathEffect.children.length > 0) { + const len = pathEffects.length; + for (let i = 0; i < len; i++) { + const pathEffect = pathEffects[i]; + const childrenLen = pathEffect.children.length; + if (childrenLen > 0) { pushPathEffects(recorder, pathEffect.children); } recorder.pushPathEffect(pathEffect.type, pathEffect.props); const needsComposition = - pathEffect.type !== NodeType.SumPathEffect && - pathEffect.children.length > 0; + pathEffect.type !== NodeType.SumPathEffect && childrenLen > 0; if (needsComposition) { recorder.composePathEffect(); } - }); + } }; const pushImageFilters = ( recorder: BaseRecorder, imageFilters: Node[] ) => { - imageFilters.forEach((imageFilter) => { - if (imageFilter.children.length > 0) { + const len = imageFilters.length; + for (let i = 0; i < len; i++) { + const imageFilter = imageFilters[i]; + const childrenLen = imageFilter.children.length; + if (childrenLen > 0) { pushImageFilters(recorder, imageFilter.children); } - if (isImageFilter(imageFilter.type)) { - recorder.pushImageFilter(imageFilter.type, imageFilter.props); - } else if (isShader(imageFilter.type)) { - recorder.pushShader(imageFilter.type, imageFilter.props, 0); + const type = imageFilter.type; + if (isImageFilter(type)) { + recorder.pushImageFilter(type, imageFilter.props); + } else if (isShader(type)) { + recorder.pushShader(type, imageFilter.props, 0); } const needsComposition = - imageFilter.type !== NodeType.BlendImageFilter && - imageFilter.children.length > 0; + type !== NodeType.BlendImageFilter && childrenLen > 0; if (needsComposition) { recorder.composeImageFilter(); } - }); + } }; const pushShaders = (recorder: BaseRecorder, shaders: Node[]) => { - shaders.forEach((shader) => { - if (shader.children.length > 0) { + const len = shaders.length; + for (let i = 0; i < len; i++) { + const shader = shaders[i]; + const childrenLen = shader.children.length; + if (childrenLen > 0) { pushShaders(recorder, shader.children); } - recorder.pushShader(shader.type, shader.props, shader.children.length); - }); + recorder.pushShader(shader.type, shader.props, childrenLen); + } }; const pushMaskFilters = (recorder: BaseRecorder, maskFilters: Node[]) => { @@ -188,7 +230,9 @@ const pushMaskFilters = (recorder: BaseRecorder, maskFilters: Node[]) => { }; const pushPaints = (recorder: BaseRecorder, paints: Node[]) => { - paints.forEach((paint) => { + const len = paints.length; + for (let i = 0; i < len; i++) { + const paint = paints[i]; recorder.savePaint(paint.props, true); const { colorFilters, maskFilters, shaders, imageFilters, pathEffects } = sortNodeChildren(paint); @@ -198,26 +242,12 @@ const pushPaints = (recorder: BaseRecorder, paints: Node[]) => { pushShaders(recorder, shaders); pushPathEffects(recorder, pathEffects); recorder.restorePaintDeclaration(); - }); -}; - -type StackingContextProps = Pick; - -const getStackingContextProps = ( - props: AnimatedProps -): AnimatedProps | undefined => { - const { zIndex } = props; - if (zIndex === undefined) { - return undefined; } - return { zIndex }; }; const visitNode = (recorder: BaseRecorder, node: Node) => { const { props } = node; - const stackingContextProps = getStackingContextProps( - props as AnimatedProps - ); + const stackingContextProps = getStackingContextForNode(node); recorder.saveGroup(stackingContextProps); const { colorFilters, @@ -228,7 +258,7 @@ const visitNode = (recorder: BaseRecorder, node: Node) => { pathEffects, paints, } = sortNodeChildren(node); - const paint = processPaint(props); + const paint = getPaintForNode(node); const shouldPushPaint = paint || colorFilters.length > 0 || @@ -254,86 +284,72 @@ const visitNode = (recorder: BaseRecorder, node: Node) => { if (node.type === NodeType.Layer) { recorder.saveLayer(); } - const ctm = processCTM(props); + const ctm = getCTMForNode(node); const shouldRestore = !!ctm || node.type === NodeType.Layer; if (ctm) { recorder.saveCTM(ctm); } - switch (node.type) { - case NodeType.Box: - const shadows = node.children - .filter((n) => n.type === NodeType.BoxShadow) - // eslint-disable-next-line @typescript-eslint/no-shadow - .map(({ props }) => ({ props }) as { props: BoxShadowProps }); - recorder.drawBox(props, shadows); - break; - case NodeType.Fill: - recorder.drawPaint(); - break; - case NodeType.Image: - recorder.drawImage(props); - break; - case NodeType.Circle: - recorder.drawCircle(props); - break; - case NodeType.Points: - recorder.drawPoints(props); - break; - case NodeType.Path: - recorder.drawPath(props); - break; - case NodeType.Rect: - recorder.drawRect(props); - break; - case NodeType.RRect: - recorder.drawRRect(props); - break; - case NodeType.Oval: - recorder.drawOval(props); - break; - case NodeType.Line: - recorder.drawLine(props); - break; - case NodeType.Patch: - recorder.drawPatch(props); - break; - case NodeType.Vertices: - recorder.drawVertices(props); - break; - case NodeType.DiffRect: - recorder.drawDiffRect(props); - break; - case NodeType.Text: - recorder.drawText(props); - break; - case NodeType.TextPath: - recorder.drawTextPath(props); - break; - case NodeType.TextBlob: - recorder.drawTextBlob(props); - break; - case NodeType.Glyphs: - recorder.drawGlyphs(props); - break; - case NodeType.Picture: - recorder.drawPicture(props); - break; - case NodeType.ImageSVG: - recorder.drawImageSVG(props); - break; - case NodeType.Paragraph: - recorder.drawParagraph(props); - break; - case NodeType.Skottie: - recorder.drawSkottie(props); - break; - case NodeType.Atlas: - recorder.drawAtlas(props); - break; + const nodeType = node.type; + // Handle special cases first + if (nodeType === NodeType.Box) { + // Optimized shadow collection - avoid filter().map() chain + const children = node.children; + const childLen = children.length; + const shadows: { props: BoxShadowProps }[] = []; + for (let i = 0; i < childLen; i++) { + const child = children[i]; + if (child.type === NodeType.BoxShadow) { + shadows.push({ props: child.props as BoxShadowProps }); + } + } + recorder.drawBox(props, shadows); + } else if (nodeType === NodeType.Fill) { + recorder.drawPaint(); + } else if (nodeType === NodeType.Image) { + recorder.drawImage(props); + } else if (nodeType === NodeType.Circle) { + recorder.drawCircle(props); + } else if (nodeType === NodeType.Points) { + recorder.drawPoints(props); + } else if (nodeType === NodeType.Path) { + recorder.drawPath(props); + } else if (nodeType === NodeType.Rect) { + recorder.drawRect(props); + } else if (nodeType === NodeType.RRect) { + recorder.drawRRect(props); + } else if (nodeType === NodeType.Oval) { + recorder.drawOval(props); + } else if (nodeType === NodeType.Line) { + recorder.drawLine(props); + } else if (nodeType === NodeType.Patch) { + recorder.drawPatch(props); + } else if (nodeType === NodeType.Vertices) { + recorder.drawVertices(props); + } else if (nodeType === NodeType.DiffRect) { + recorder.drawDiffRect(props); + } else if (nodeType === NodeType.Text) { + recorder.drawText(props); + } else if (nodeType === NodeType.TextPath) { + recorder.drawTextPath(props); + } else if (nodeType === NodeType.TextBlob) { + recorder.drawTextBlob(props); + } else if (nodeType === NodeType.Glyphs) { + recorder.drawGlyphs(props); + } else if (nodeType === NodeType.Picture) { + recorder.drawPicture(props); + } else if (nodeType === NodeType.ImageSVG) { + recorder.drawImageSVG(props); + } else if (nodeType === NodeType.Paragraph) { + recorder.drawParagraph(props); + } else if (nodeType === NodeType.Skottie) { + recorder.drawSkottie(props); + } else if (nodeType === NodeType.Atlas) { + recorder.drawAtlas(props); + } + const drawingsLen = drawings.length; + for (let i = 0; i < drawingsLen; i++) { + visitNode(recorder, drawings[i]); } - drawings.forEach((drawing) => { - visitNode(recorder, drawing); - }); if (shouldPushPaint) { recorder.restorePaint(); } @@ -344,7 +360,8 @@ const visitNode = (recorder: BaseRecorder, node: Node) => { }; export const visit = (recorder: BaseRecorder, root: Node[]) => { - root.forEach((node) => { - visitNode(recorder, node); - }); + const len = root.length; + for (let i = 0; i < len; i++) { + visitNode(recorder, root[i]); + } }; From 2a148916c1d5d805e0c958fe43cb1b21e8ce92f8 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 9 Dec 2025 15:08:43 +0100 Subject: [PATCH 2/2] :wrench: --- packages/skia/cpp/api/recorder/JsiRecorder.h | 14 +- packages/skia/cpp/api/recorder/RNRecorder.h | 478 ++++++++++++++++++ .../__tests__/e2e/VisitPerformance.spec.tsx | 258 ++++++++++ packages/skia/src/skia/types/Recorder.ts | 7 + packages/skia/src/sksg/Container.native.ts | 19 +- 5 files changed, 764 insertions(+), 12 deletions(-) create mode 100644 packages/skia/src/renderer/__tests__/e2e/VisitPerformance.spec.tsx diff --git a/packages/skia/cpp/api/recorder/JsiRecorder.h b/packages/skia/cpp/api/recorder/JsiRecorder.h index 46ca09ecd0..08cc36b842 100644 --- a/packages/skia/cpp/api/recorder/JsiRecorder.h +++ b/packages/skia/cpp/api/recorder/JsiRecorder.h @@ -302,6 +302,16 @@ class JsiRecorder : public JsiSkWrappingSharedPtrHostObject { return jsi::Value::undefined(); } + JSI_HOST_FUNCTION(visit) { + auto root = arguments[0].asObject(runtime).asArray(runtime); + auto sharedValues = getObject()->visit(runtime, root); + return std::move(sharedValues); + } + + JSI_HOST_FUNCTION(getVariableCount) { + return jsi::Value(static_cast(getObject()->getVariableCount())); + } + EXPORT_JSI_API_TYPENAME(JsiRecorder, Recorder) JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiRecorder, saveGroup), @@ -346,7 +356,9 @@ class JsiRecorder : public JsiSkWrappingSharedPtrHostObject { JSI_EXPORT_FUNC(JsiRecorder, drawSkottie), JSI_EXPORT_FUNC(JsiRecorder, play), JSI_EXPORT_FUNC(JsiRecorder, applyUpdates), - JSI_EXPORT_FUNC(JsiRecorder, reset)) + JSI_EXPORT_FUNC(JsiRecorder, reset), + JSI_EXPORT_FUNC(JsiRecorder, visit), + JSI_EXPORT_FUNC(JsiRecorder, getVariableCount)) // This has no basis in reality but since since these are private long-lived // objects, we think it is more than fine. diff --git a/packages/skia/cpp/api/recorder/RNRecorder.h b/packages/skia/cpp/api/recorder/RNRecorder.h index 8eb7bc9993..3f488e91f1 100644 --- a/packages/skia/cpp/api/recorder/RNRecorder.h +++ b/packages/skia/cpp/api/recorder/RNRecorder.h @@ -406,6 +406,484 @@ class Recorder { playCommand(ctx, cmd.get()); } } + + // Returns the number of unique shared values (variables) recorded + size_t getVariableCount() const { + return variables.size(); + } + + // C++ visitor - traverses node tree directly without JS overhead + // Returns an array of shared values found during traversal + jsi::Array visit(jsi::Runtime &runtime, const jsi::Array &root) { + // Collect shared values during visit + std::vector sharedValues; + + size_t len = root.size(runtime); + for (size_t i = 0; i < len; i++) { + auto node = root.getValueAtIndex(runtime, i).asObject(runtime); + visitNode(runtime, node, sharedValues); + } + + // Return the collected shared values as a JSI array + jsi::Array result(runtime, sharedValues.size()); + for (size_t i = 0; i < sharedValues.size(); i++) { + result.setValueAtIndex(runtime, i, std::move(sharedValues[i])); + } + return result; + } + +private: + // Helper to check if a value is a Reanimated shared value + static bool isSharedValue(jsi::Runtime &runtime, const jsi::Value &value) { + if (!value.isObject()) { + return false; + } + auto obj = value.asObject(runtime); + if (!obj.hasProperty(runtime, "_isReanimatedSharedValue")) { + return false; + } + auto prop = obj.getProperty(runtime, "_isReanimatedSharedValue"); + return prop.isBool() && prop.asBool(); + } + + // Collect shared values from a props object and assign names to them + void collectSharedValues(jsi::Runtime &runtime, const jsi::Object &props, + std::vector &sharedValues) { + auto propertyNames = props.getPropertyNames(runtime); + size_t propCount = propertyNames.size(runtime); + + for (size_t i = 0; i < propCount; i++) { + auto propName = propertyNames.getValueAtIndex(runtime, i).asString(runtime); + auto propValue = props.getProperty(runtime, propName); + + if (isSharedValue(runtime, propValue)) { + auto sharedValue = propValue.asObject(runtime); + + // Check if this shared value already has a name assigned + if (!sharedValue.hasProperty(runtime, "name") || + sharedValue.getProperty(runtime, "name").isUndefined()) { + // Assign a new name based on current count + std::string name = "variable" + std::to_string(sharedValues.size()); + sharedValue.setProperty(runtime, "name", + jsi::String::createFromUtf8(runtime, name)); + sharedValues.push_back(std::move(sharedValue)); + } else { + // Already named, check if we need to add it to our list + auto existingName = sharedValue.getProperty(runtime, "name") + .asString(runtime) + .utf8(runtime); + // Check if it starts with "variable" - if so, it's already tracked + if (existingName.rfind("variable", 0) != 0) { + // New shared value with non-standard name, assign proper name + std::string name = "variable" + std::to_string(sharedValues.size()); + sharedValue.setProperty(runtime, "name", + jsi::String::createFromUtf8(runtime, name)); + sharedValues.push_back(std::move(sharedValue)); + } + } + } + } + } + + // Node type classification helpers + static bool isColorFilter(const std::string &type) { + return type == "skBlendColorFilter" || type == "skMatrixColorFilter" || + type == "skLerpColorFilter" || type == "skLumaColorFilter" || + type == "skSRGBToLinearGammaColorFilter" || + type == "skLinearToSRGBGammaColorFilter"; + } + + static bool isPathEffect(const std::string &type) { + return type == "skDiscretePathEffect" || type == "skDashPathEffect" || + type == "skPath1DPathEffect" || type == "skPath2DPathEffect" || + type == "skCornerPathEffect" || type == "skSumPathEffect" || + type == "skLine2DPathEffect"; + } + + static bool isImageFilterType(const std::string &type) { + return type == "skImageFilter" || type == "skOffsetImageFilter" || + type == "skDisplacementMapImageFilter" || + type == "skBlurImageFilter" || type == "skDropShadowImageFilter" || + type == "skMorphologyImageFilter" || type == "skBlendImageFilter" || + type == "skRuntimeShaderImageFilter"; + } + + static bool isShader(const std::string &type) { + return type == "skShader" || type == "skImageShader" || + type == "skColorShader" || type == "skTurbulence" || + type == "skFractalNoise" || type == "skLinearGradient" || + type == "skRadialGradient" || type == "skSweepGradient" || + type == "skTwoPointConicalGradient"; + } + + // Check if props object has any paint-related properties + static bool hasPaintProps(jsi::Runtime &runtime, const jsi::Object &props) { + return props.hasProperty(runtime, "opacity") || + props.hasProperty(runtime, "color") || + props.hasProperty(runtime, "strokeWidth") || + props.hasProperty(runtime, "blendMode") || + props.hasProperty(runtime, "style") || + props.hasProperty(runtime, "strokeJoin") || + props.hasProperty(runtime, "strokeCap") || + props.hasProperty(runtime, "strokeMiter") || + props.hasProperty(runtime, "antiAlias") || + props.hasProperty(runtime, "dither") || + props.hasProperty(runtime, "paint"); + } + + // Check if props object has any CTM-related properties + static bool hasCTMProps(jsi::Runtime &runtime, const jsi::Object &props) { + return props.hasProperty(runtime, "clip") || + props.hasProperty(runtime, "invertClip") || + props.hasProperty(runtime, "transform") || + props.hasProperty(runtime, "origin") || + props.hasProperty(runtime, "matrix") || + props.hasProperty(runtime, "layer"); + } + + // Sort children into categories + struct SortedChildren { + std::vector colorFilters; + std::vector maskFilters; + std::vector shaders; + std::vector imageFilters; + std::vector pathEffects; + std::vector drawings; + std::vector paints; + }; + + SortedChildren sortNodeChildren(jsi::Runtime &runtime, + const jsi::Object &node) { + SortedChildren result; + + if (!node.hasProperty(runtime, "children")) { + return result; + } + + auto children = node.getProperty(runtime, "children").asObject(runtime).asArray(runtime); + size_t len = children.size(runtime); + + for (size_t i = 0; i < len; i++) { + auto child = children.getValueAtIndex(runtime, i).asObject(runtime); + auto type = child.getProperty(runtime, "type").asString(runtime).utf8(runtime); + + if (isColorFilter(type)) { + result.colorFilters.push_back(std::move(child)); + } else if (type == "skBlurMaskFilter") { + result.maskFilters.push_back(std::move(child)); + } else if (isPathEffect(type)) { + result.pathEffects.push_back(std::move(child)); + } else if (isImageFilterType(type)) { + result.imageFilters.push_back(std::move(child)); + } else if (isShader(type)) { + result.shaders.push_back(std::move(child)); + } else if (type == "skPaint") { + result.paints.push_back(std::move(child)); + } else if (type == "skBlend") { + // Check first child to determine if it's an image filter blend or shader blend + if (child.hasProperty(runtime, "children")) { + auto blendChildren = child.getProperty(runtime, "children").asObject(runtime).asArray(runtime); + if (blendChildren.size(runtime) > 0) { + auto firstChild = blendChildren.getValueAtIndex(runtime, 0).asObject(runtime); + auto firstChildType = firstChild.getProperty(runtime, "type").asString(runtime).utf8(runtime); + if (isImageFilterType(firstChildType)) { + // Mutate type to BlendImageFilter + child.setProperty(runtime, "type", jsi::String::createFromUtf8(runtime, "skBlendImageFilter")); + result.imageFilters.push_back(std::move(child)); + } else { + result.shaders.push_back(std::move(child)); + } + } else { + result.shaders.push_back(std::move(child)); + } + } else { + result.shaders.push_back(std::move(child)); + } + } else { + result.drawings.push_back(std::move(child)); + } + } + + return result; + } + + void pushColorFilters(jsi::Runtime &runtime, + std::vector &colorFilters, + std::vector &sharedValues) { + for (auto &colorFilter : colorFilters) { + // Recurse on children first + if (colorFilter.hasProperty(runtime, "children")) { + auto children = colorFilter.getProperty(runtime, "children").asObject(runtime).asArray(runtime); + size_t len = children.size(runtime); + std::vector childFilters; + for (size_t i = 0; i < len; i++) { + childFilters.push_back(children.getValueAtIndex(runtime, i).asObject(runtime)); + } + pushColorFilters(runtime, childFilters, sharedValues); + } + + auto type = colorFilter.getProperty(runtime, "type").asString(runtime).utf8(runtime); + auto props = colorFilter.getProperty(runtime, "props").asObject(runtime); + collectSharedValues(runtime, props, sharedValues); + pushColorFilter(runtime, type, props); + + // Check if composition needed + bool hasChildren = colorFilter.hasProperty(runtime, "children") && + colorFilter.getProperty(runtime, "children").asObject(runtime).asArray(runtime).size(runtime) > 0; + bool needsComposition = type != "skLerpColorFilter" && hasChildren; + if (needsComposition) { + composeColorFilter(); + } + } + } + + void pushPathEffects(jsi::Runtime &runtime, + std::vector &pathEffects, + std::vector &sharedValues) { + for (auto &pathEffect : pathEffects) { + if (pathEffect.hasProperty(runtime, "children")) { + auto children = pathEffect.getProperty(runtime, "children").asObject(runtime).asArray(runtime); + size_t len = children.size(runtime); + std::vector childEffects; + for (size_t i = 0; i < len; i++) { + childEffects.push_back(children.getValueAtIndex(runtime, i).asObject(runtime)); + } + pushPathEffects(runtime, childEffects, sharedValues); + } + + auto type = pathEffect.getProperty(runtime, "type").asString(runtime).utf8(runtime); + auto props = pathEffect.getProperty(runtime, "props").asObject(runtime); + collectSharedValues(runtime, props, sharedValues); + pushPathEffect(runtime, type, props); + + bool hasChildren = pathEffect.hasProperty(runtime, "children") && + pathEffect.getProperty(runtime, "children").asObject(runtime).asArray(runtime).size(runtime) > 0; + bool needsComposition = type != "skSumPathEffect" && hasChildren; + if (needsComposition) { + composePathEffect(); + } + } + } + + void pushImageFilters(jsi::Runtime &runtime, + std::vector &imageFilters, + std::vector &sharedValues) { + for (auto &imageFilter : imageFilters) { + if (imageFilter.hasProperty(runtime, "children")) { + auto children = imageFilter.getProperty(runtime, "children").asObject(runtime).asArray(runtime); + size_t len = children.size(runtime); + std::vector childFilters; + for (size_t i = 0; i < len; i++) { + childFilters.push_back(children.getValueAtIndex(runtime, i).asObject(runtime)); + } + pushImageFilters(runtime, childFilters, sharedValues); + } + + auto type = imageFilter.getProperty(runtime, "type").asString(runtime).utf8(runtime); + auto props = imageFilter.getProperty(runtime, "props").asObject(runtime); + collectSharedValues(runtime, props, sharedValues); + + if (isImageFilterType(type)) { + pushImageFilter(runtime, type, props); + } else if (isShader(type)) { + pushShader(runtime, type, props, 0); + } + + bool hasChildren = imageFilter.hasProperty(runtime, "children") && + imageFilter.getProperty(runtime, "children").asObject(runtime).asArray(runtime).size(runtime) > 0; + bool needsComposition = type != "skBlendImageFilter" && hasChildren; + if (needsComposition) { + composeImageFilter(); + } + } + } + + void pushShaders(jsi::Runtime &runtime, std::vector &shaders, + std::vector &sharedValues) { + for (auto &shader : shaders) { + size_t childrenLen = 0; + if (shader.hasProperty(runtime, "children")) { + auto children = shader.getProperty(runtime, "children").asObject(runtime).asArray(runtime); + childrenLen = children.size(runtime); + std::vector childShaders; + for (size_t i = 0; i < childrenLen; i++) { + childShaders.push_back(children.getValueAtIndex(runtime, i).asObject(runtime)); + } + pushShaders(runtime, childShaders, sharedValues); + } + + auto type = shader.getProperty(runtime, "type").asString(runtime).utf8(runtime); + auto props = shader.getProperty(runtime, "props").asObject(runtime); + collectSharedValues(runtime, props, sharedValues); + pushShader(runtime, type, props, static_cast(childrenLen)); + } + } + + void pushMaskFilters(jsi::Runtime &runtime, + std::vector &maskFilters, + std::vector &sharedValues) { + if (!maskFilters.empty()) { + auto &last = maskFilters.back(); + auto props = last.getProperty(runtime, "props").asObject(runtime); + collectSharedValues(runtime, props, sharedValues); + pushBlurMaskFilter(runtime, props); + } + } + + void pushPaints(jsi::Runtime &runtime, std::vector &paints, + std::vector &sharedValues) { + for (auto &paint : paints) { + auto props = paint.getProperty(runtime, "props").asObject(runtime); + collectSharedValues(runtime, props, sharedValues); + savePaint(runtime, props, true); + + auto sorted = sortNodeChildren(runtime, paint); + pushColorFilters(runtime, sorted.colorFilters, sharedValues); + pushImageFilters(runtime, sorted.imageFilters, sharedValues); + pushMaskFilters(runtime, sorted.maskFilters, sharedValues); + pushShaders(runtime, sorted.shaders, sharedValues); + pushPathEffects(runtime, sorted.pathEffects, sharedValues); + + restorePaintDeclaration(); + } + } + + void visitNode(jsi::Runtime &runtime, jsi::Object &node, + std::vector &sharedValues) { + auto type = node.getProperty(runtime, "type").asString(runtime).utf8(runtime); + auto props = node.getProperty(runtime, "props").asObject(runtime); + + // Collect shared values from this node's props + collectSharedValues(runtime, props, sharedValues); + + // Get stacking context props (zIndex) + if (props.hasProperty(runtime, "zIndex")) { + jsi::Value propsValue = jsi::Value(runtime, props); + saveGroup(runtime, &propsValue); + } else { + saveGroup(runtime, nullptr); + } + + // Sort children + auto sorted = sortNodeChildren(runtime, node); + + // Check if we need to push paint + bool shouldPushPaint = hasPaintProps(runtime, props) || + !sorted.colorFilters.empty() || + !sorted.maskFilters.empty() || + !sorted.imageFilters.empty() || + !sorted.pathEffects.empty() || + !sorted.shaders.empty(); + + if (shouldPushPaint) { + savePaint(runtime, props, false); + pushColorFilters(runtime, sorted.colorFilters, sharedValues); + pushImageFilters(runtime, sorted.imageFilters, sharedValues); + pushMaskFilters(runtime, sorted.maskFilters, sharedValues); + pushShaders(runtime, sorted.shaders, sharedValues); + pushPathEffects(runtime, sorted.pathEffects, sharedValues); + + if (type == "skBackdropFilter") { + saveBackdropFilter(); + } else { + materializePaint(); + } + } + + // Push standalone paints + pushPaints(runtime, sorted.paints, sharedValues); + + // Handle Layer + if (type == "skLayer") { + saveLayer(); + } + + // Handle CTM + bool hasCTM = hasCTMProps(runtime, props); + bool shouldRestore = hasCTM || type == "skLayer"; + if (hasCTM) { + saveCTM(runtime, props); + } + + // Draw based on node type + if (type == "skBox") { + // Collect box shadows + jsi::Array shadows = jsi::Array(runtime, 0); + if (node.hasProperty(runtime, "children")) { + auto children = node.getProperty(runtime, "children").asObject(runtime).asArray(runtime); + size_t len = children.size(runtime); + std::vector shadowProps; + for (size_t i = 0; i < len; i++) { + auto child = children.getValueAtIndex(runtime, i).asObject(runtime); + auto childType = child.getProperty(runtime, "type").asString(runtime).utf8(runtime); + if (childType == "skBoxShadow") { + shadowProps.push_back(child.getProperty(runtime, "props")); + } + } + shadows = jsi::Array(runtime, shadowProps.size()); + for (size_t i = 0; i < shadowProps.size(); i++) { + shadows.setValueAtIndex(runtime, i, std::move(shadowProps[i])); + } + } + drawBox(runtime, props, shadows); + } else if (type == "skFill") { + drawPaint(); + } else if (type == "skImage") { + drawImage(runtime, props); + } else if (type == "skCircle") { + drawCircle(runtime, props); + } else if (type == "skPoints") { + drawPoints(runtime, props); + } else if (type == "skPath") { + drawPath(runtime, props); + } else if (type == "skRect") { + drawRect(runtime, props); + } else if (type == "skRRect") { + drawRRect(runtime, props); + } else if (type == "skOval") { + drawOval(runtime, props); + } else if (type == "skLine") { + drawLine(runtime, props); + } else if (type == "skPatch") { + drawPatch(runtime, props); + } else if (type == "skVertices") { + drawVertices(runtime, props); + } else if (type == "skDiffRect") { + drawDiffRect(runtime, props); + } else if (type == "skText") { + drawText(runtime, props); + } else if (type == "skTextPath") { + drawTextPath(runtime, props); + } else if (type == "skTextBlob") { + drawTextBlob(runtime, props); + } else if (type == "skGlyphs") { + drawGlyphs(runtime, props); + } else if (type == "skPicture") { + drawPicture(runtime, props); + } else if (type == "skImageSVG") { + drawImageSVG(runtime, props); + } else if (type == "skParagraph") { + drawParagraph(runtime, props); + } else if (type == "skSkottie") { + drawSkottie(runtime, props); + } else if (type == "skAtlas") { + drawAtlas(runtime, props); + } + + // Visit drawing children + for (auto &drawing : sorted.drawings) { + visitNode(runtime, drawing, sharedValues); + } + + // Restore state + if (shouldPushPaint) { + restorePaint(); + } + if (shouldRestore) { + restoreCTM(); + } + restoreGroup(); + } }; inline void Recorder::playCommand(DrawingCtx *ctx, Command *cmd) { diff --git a/packages/skia/src/renderer/__tests__/e2e/VisitPerformance.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/VisitPerformance.spec.tsx new file mode 100644 index 0000000000..760576894d --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/VisitPerformance.spec.tsx @@ -0,0 +1,258 @@ +import React from "react"; + +import { Group, Rect, Circle, Fill } from "../../components"; +import { surface, mountCanvas } from "../setup"; +import { visit } from "../../../sksg/Recorder/Visitor"; +import { Recorder } from "../../../sksg/Recorder/Recorder"; + +// Helper to create a deeply nested tree structure +const DeepTree = ({ + depth, + breadth, + currentDepth = 0, +}: { + depth: number; + breadth: number; + currentDepth?: number; +}): React.ReactElement => { + if (currentDepth >= depth) { + // Leaf nodes - simple shapes + return ( + + + + + ); + } + + // Create multiple children at each level + const children: React.ReactElement[] = []; + for (let i = 0; i < breadth; i++) { + children.push( + + + + ); + } + + return ( + + {children} + + ); +}; + +// Helper to create a wide tree with many siblings +const WideTree = ({ count }: { count: number }): React.ReactElement => { + const children: React.ReactElement[] = []; + for (let i = 0; i < count; i++) { + children.push( + + + + + ); + } + return {children}; +}; + +// Helper to create a complex tree with mixed content +const ComplexTree = ({ + groups, + shapesPerGroup, +}: { + groups: number; + shapesPerGroup: number; +}): React.ReactElement => { + const groupElements: React.ReactElement[] = []; + for (let g = 0; g < groups; g++) { + const shapes: React.ReactElement[] = []; + for (let s = 0; s < shapesPerGroup; s++) { + const x = (g * shapesPerGroup + s) % 256; + const y = Math.floor((g * shapesPerGroup + s) / 256); + shapes.push( + + + + ); + } + groupElements.push( + + {shapes} + + ); + } + return ( + + + {groupElements} + + ); +}; + +// Benchmark results collector +interface BenchmarkResult { + name: string; + nodes: string; + iterations: number; + avg: number; + min: number; + max: number; +} + +const results: BenchmarkResult[] = []; + +const runBenchmark = ( + name: string, + nodes: string, + iterations: number, + fn: () => void +): BenchmarkResult => { + const times: number[] = []; + + for (let i = 0; i < iterations; i++) { + const start = performance.now(); + fn(); + const end = performance.now(); + times.push(end - start); + } + + const avg = times.reduce((a, b) => a + b, 0) / times.length; + const min = Math.min(...times); + const max = Math.max(...times); + + return { name, nodes, iterations, avg, min, max }; +}; + +const printResultsTable = () => { + // eslint-disable-next-line no-console + console.log("\n┌─────────────────────────────────────────────────────────────────────────────────────┐"); + // eslint-disable-next-line no-console + console.log("│ Visit Performance Benchmark │"); + // eslint-disable-next-line no-console + console.log("├──────────────────────────────┬──────────┬──────┬───────────┬───────────┬───────────┤"); + // eslint-disable-next-line no-console + console.log("│ Test Name │ Nodes │ Runs │ Avg (ms) │ Min (ms) │ Max (ms) │"); + // eslint-disable-next-line no-console + console.log("├──────────────────────────────┼──────────┼──────┼───────────┼───────────┼───────────┤"); + + for (const r of results) { + const name = r.name.padEnd(28); + const nodes = r.nodes.padStart(8); + const runs = r.iterations.toString().padStart(4); + const avg = r.avg.toFixed(3).padStart(9); + const min = r.min.toFixed(3).padStart(9); + const max = r.max.toFixed(3).padStart(9); + // eslint-disable-next-line no-console + console.log(`│ ${name} │ ${nodes} │ ${runs} │ ${avg} │ ${min} │ ${max} │`); + } + + // eslint-disable-next-line no-console + console.log("└──────────────────────────────┴──────────┴──────┴───────────┴───────────┴───────────┘\n"); +}; + +describe("Visit Performance", () => { + afterAll(() => { + printResultsTable(); + }); + + it("should handle a deeply nested tree (depth=7, breadth=3)", async () => { + const { root, render } = await mountCanvas( + + ); + await render(); + + const recorder = new Recorder(); + const result = runBenchmark( + "Deep Tree (d=7, b=3)", + "~2000", + 50, + () => visit(recorder, root.sg.children) + ); + results.push(result); + + expect(result.avg).toBeDefined(); + }); + + it("should handle a wide tree (1500 siblings)", async () => { + const { root, render } = await mountCanvas(); + await render(); + + const recorder = new Recorder(); + const result = runBenchmark( + "Wide Tree (1500 siblings)", + "~4500", + 50, + () => visit(recorder, root.sg.children) + ); + results.push(result); + + expect(result.avg).toBeDefined(); + }); + + it("should handle a complex tree (60 groups x 25 shapes)", async () => { + const { root, render } = await mountCanvas( + + ); + await render(); + + const recorder = new Recorder(); + const result = runBenchmark( + "Complex Tree (60x25)", + "~3000", + 50, + () => visit(recorder, root.sg.children) + ); + results.push(result); + + expect(result.avg).toBeDefined(); + }); + + it("should handle large flat tree (3000 nodes)", async () => { + const { root, render } = await mountCanvas(); + await render(); + + const recorder = new Recorder(); + const result = runBenchmark( + "Large Flat Tree", + "~9000", + 30, + () => visit(recorder, root.sg.children) + ); + results.push(result); + + expect(result.avg).toBeDefined(); + }); + + it("benchmark: deeply nested with transforms (depth=11, breadth=2)", async () => { + const { root, render } = await mountCanvas( + + ); + await render(); + + const recorder = new Recorder(); + const result = runBenchmark( + "Deep Transforms (d=11, b=2)", + "~2000", + 30, + () => visit(recorder, root.sg.children) + ); + results.push(result); + + expect(result.avg).toBeDefined(); + }); + + it("should render correctly with a deep tree", async () => { + const img = await surface.draw(); + expect(img).toBeTruthy(); + expect(img.width()).toBeGreaterThan(0); + expect(img.height()).toBeGreaterThan(0); + }); +}); diff --git a/packages/skia/src/skia/types/Recorder.ts b/packages/skia/src/skia/types/Recorder.ts index 008814e3ef..c0072c34ae 100644 --- a/packages/skia/src/skia/types/Recorder.ts +++ b/packages/skia/src/skia/types/Recorder.ts @@ -32,6 +32,7 @@ import type { import type { AnimatedProps } from "../../renderer/processors/Animations/Animations"; import type { SkPicture } from "./Picture"; +import type { Node } from "../../sksg/Node"; export interface BaseRecorder { saveGroup(props?: AnimatedProps>): void; @@ -96,4 +97,10 @@ export interface JsiRecorder extends BaseRecorder { play(picture: SkPicture): void; applyUpdates(variables: SharedValue[]): void; reset(): void; + /** + * Native C++ visitor - traverses the node tree and records commands. + * Returns an array of shared values found during traversal. + */ + visit(root: Node[]): SharedValue[]; + getVariableCount(): number; } diff --git a/packages/skia/src/sksg/Container.native.ts b/packages/skia/src/sksg/Container.native.ts index 85a2feb561..0855154e3f 100644 --- a/packages/skia/src/sksg/Container.native.ts +++ b/packages/skia/src/sksg/Container.native.ts @@ -1,11 +1,8 @@ import Rea from "../external/reanimated/ReanimatedProxy"; -import type { Skia, SkPicture } from "../skia/types"; +import type { Skia, SkPicture, JsiRecorder } from "../skia/types"; import { HAS_REANIMATED_3 } from "../external/reanimated/renderHelpers"; -import type { JsiRecorder } from "../skia/types/Recorder"; -import { ReanimatedRecorder } from "./Recorder/ReanimatedRecorder"; import { Container, StaticContainer } from "./StaticContainer"; -import { visit } from "./Recorder/Visitor"; import "../skia/NativeSetup"; import "../views/api"; @@ -43,23 +40,23 @@ class NativeReanimatedContainer extends Container { if (this.unmounted) { return; } - const recorder = new ReanimatedRecorder(this.Skia); + // Use native C++ visit for faster tree traversal + const recorder = this.Skia.Recorder(); const { nativeId, picture, Skia } = this; - visit(recorder, this.root); - const sharedValues = recorder.getSharedValues(); - const sharedRecorder = recorder.getRecorder(); + // Native visit returns the shared values it found + const sharedValues = recorder.visit(this.root); // Draw first frame Rea.executeOnUIRuntimeSync(() => { "worklet"; const firstPicture = Skia.Picture.MakePicture(null)!; - nativeDrawOnscreen(nativeId, sharedRecorder, firstPicture); + nativeDrawOnscreen(nativeId, recorder, firstPicture); })(); // Animate if (sharedValues.length > 0) { this.mapperId = Rea.startMapper(() => { "worklet"; - sharedRecorder.applyUpdates(sharedValues); - nativeDrawOnscreen(nativeId, sharedRecorder, picture); + recorder.applyUpdates(sharedValues); + nativeDrawOnscreen(nativeId, recorder, picture); }, sharedValues); } }