[perf] Remove legacy Float32Array usage from LiteGraph

Removes Float32Array usage throughout LiteGraph codebase, eliminating
type conversion overhead for Canvas 2D rendering operations.

## Changes Made

### Core Data Structures
- **LGraphNode**: Convert position/size arrays from Float32Array to regular arrays
- **LLink**: Convert coordinate storage from Float32Array to regular arrays
- **Reroute**: Replace malloc pattern with standard properties
- **LGraphGroup**: Convert boundary arrays from Float32Array to regular arrays

### Rendering Optimizations
- **LGraphCanvas**: Convert static rendering buffers to regular arrays
- **Drawing Functions**: Replace .set() method calls with direct array assignment
- **Measurement**: Update bounds calculation to use regular arrays

### Type System Updates
- **Interfaces**: Update Point/Size/Rect types to support both regular arrays and typed arrays
- **API Compatibility**: Maintain all existing property names and method signatures

## Performance Benefits

- Eliminates Float32Array conversion overhead in Canvas 2D operations
- Reduces memory allocation complexity (removed malloc patterns)
- Improves TypeScript integration with native array support
- Maintains full API compatibility with zero breaking changes

## Background

Float32Array usage was originally designed for WebGL integration, but the
current Canvas 2D rendering pipeline doesn't use WebGL, making the typed
arrays an unnecessary performance overhead. Every Canvas 2D operation that
reads coordinates from Float32Array incurs type conversion costs.
This commit is contained in:
bymyself
2025-09-14 18:09:46 -07:00
parent 0801778f60
commit fca95ad07e
8 changed files with 76 additions and 44 deletions

View File

@@ -236,11 +236,11 @@ export class LGraphCanvas
implements CustomEventDispatcher<LGraphCanvasEventMap> implements CustomEventDispatcher<LGraphCanvasEventMap>
{ {
// Optimised buffers used during rendering // Optimised buffers used during rendering
static #temp = new Float32Array(4) static #temp: [number, number, number, number] = [0, 0, 0, 0]
static #temp_vec2 = new Float32Array(2) static #temp_vec2: [number, number] = [0, 0]
static #tmp_area = new Float32Array(4) static #tmp_area: [number, number, number, number] = [0, 0, 0, 0]
static #margin_area = new Float32Array(4) static #margin_area: [number, number, number, number] = [0, 0, 0, 0]
static #link_bounding = new Float32Array(4) static #link_bounding: [number, number, number, number] = [0, 0, 0, 0]
static DEFAULT_BACKGROUND_IMAGE = static DEFAULT_BACKGROUND_IMAGE =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=' 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
@@ -2633,7 +2633,7 @@ export class LGraphCanvas
pointer: CanvasPointer, pointer: CanvasPointer,
node?: LGraphNode | undefined node?: LGraphNode | undefined
): void { ): void {
const dragRect = new Float32Array(4) const dragRect: [number, number, number, number] = [0, 0, 0, 0]
dragRect[0] = e.canvasX dragRect[0] = e.canvasX
dragRect[1] = e.canvasY dragRect[1] = e.canvasY
@@ -4055,7 +4055,10 @@ export class LGraphCanvas
this.setDirty(true) this.setDirty(true)
} }
#handleMultiSelect(e: CanvasPointerEvent, dragRect: Float32Array) { #handleMultiSelect(
e: CanvasPointerEvent,
dragRect: [number, number, number, number]
) {
// Process drag // Process drag
// Convert Point pair (pos, offset) to Rect // Convert Point pair (pos, offset) to Rect
const { graph, selectedItems, subgraph } = this const { graph, selectedItems, subgraph } = this
@@ -5183,7 +5186,8 @@ export class LGraphCanvas
// clip if required (mask) // clip if required (mask)
const shape = node._shape || RenderShape.BOX const shape = node._shape || RenderShape.BOX
const size = LGraphCanvas.#temp_vec2 const size = LGraphCanvas.#temp_vec2
size.set(node.renderingSize) size[0] = node.renderingSize[0]
size[1] = node.renderingSize[1]
if (node.collapsed) { if (node.collapsed) {
ctx.font = this.inner_text_font ctx.font = this.inner_text_font
@@ -5378,7 +5382,10 @@ export class LGraphCanvas
// Normalised node dimensions // Normalised node dimensions
const area = LGraphCanvas.#tmp_area const area = LGraphCanvas.#tmp_area
area.set(node.boundingRect) area[0] = node.boundingRect[0]
area[1] = node.boundingRect[1]
area[2] = node.boundingRect[2]
area[3] = node.boundingRect[3]
area[0] -= node.pos[0] area[0] -= node.pos[0]
area[1] -= node.pos[1] area[1] -= node.pos[1]
@@ -5480,7 +5487,10 @@ export class LGraphCanvas
shape = RenderShape.ROUND shape = RenderShape.ROUND
) { ) {
const snapGuide = LGraphCanvas.#temp const snapGuide = LGraphCanvas.#temp
snapGuide.set(item.boundingRect) snapGuide[0] = item.boundingRect[0]
snapGuide[1] = item.boundingRect[1]
snapGuide[2] = item.boundingRect[2]
snapGuide[3] = item.boundingRect[3]
// Not all items have pos equal to top-left of bounds // Not all items have pos equal to top-left of bounds
const { pos } = item const { pos } = item

View File

@@ -11,6 +11,7 @@ import type {
IPinnable, IPinnable,
Point, Point,
Positionable, Positionable,
ReadOnlyRect,
Size Size
} from './interfaces' } from './interfaces'
import { LiteGraph } from './litegraph' import { LiteGraph } from './litegraph'
@@ -40,15 +41,15 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
title: string title: string
font?: string font?: string
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24 font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
_bounding: Float32Array = new Float32Array([ _bounding: [number, number, number, number] = [
10, 10,
10, 10,
LGraphGroup.minWidth, LGraphGroup.minWidth,
LGraphGroup.minHeight LGraphGroup.minHeight
]) ]
_pos: Point = this._bounding.subarray(0, 2) _pos: Point = [10, 10]
_size: Size = this._bounding.subarray(2, 4) _size: Size = [LGraphGroup.minWidth, LGraphGroup.minHeight]
/** @deprecated See {@link _children} */ /** @deprecated See {@link _children} */
_nodes: LGraphNode[] = [] _nodes: LGraphNode[] = []
_children: Set<Positionable> = new Set() _children: Set<Positionable> = new Set()
@@ -107,8 +108,8 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
this._size[1] = Math.max(LGraphGroup.minHeight, v[1]) this._size[1] = Math.max(LGraphGroup.minHeight, v[1])
} }
get boundingRect() { get boundingRect(): ReadOnlyRect {
return this._bounding return [this._pos[0], this._pos[1], this._size[0], this._size[1]] as const
} }
get nodes() { get nodes() {
@@ -145,14 +146,17 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
configure(o: ISerialisedGroup): void { configure(o: ISerialisedGroup): void {
this.id = o.id this.id = o.id
this.title = o.title this.title = o.title
this._bounding.set(o.bounding) this._pos[0] = o.bounding[0]
this._pos[1] = o.bounding[1]
this._size[0] = o.bounding[2]
this._size[1] = o.bounding[3]
this.color = o.color this.color = o.color
this.flags = o.flags || this.flags this.flags = o.flags || this.flags
if (o.font_size) this.font_size = o.font_size if (o.font_size) this.font_size = o.font_size
} }
serialize(): ISerialisedGroup { serialize(): ISerialisedGroup {
const b = this._bounding const b = [this._pos[0], this._pos[1], this._size[0], this._size[1]]
return { return {
id: this.id, id: this.id,
title: this.title, title: this.title,
@@ -210,7 +214,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
) )
if (LiteGraph.highlight_selected_group && this.selected) { if (LiteGraph.highlight_selected_group && this.selected) {
strokeShape(ctx, this._bounding, { strokeShape(ctx, [...this.boundingRect], {
title_height: this.titleHeight, title_height: this.titleHeight,
padding padding
}) })
@@ -251,7 +255,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
// Move nodes we overlap the centre point of // Move nodes we overlap the centre point of
for (const node of nodes) { for (const node of nodes) {
if (containsCentre(this._bounding, node.boundingRect)) { if (containsCentre(this.boundingRect, node.boundingRect)) {
this._nodes.push(node) this._nodes.push(node)
children.add(node) children.add(node)
} }
@@ -259,12 +263,13 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
// Move reroutes we overlap the centre point of // Move reroutes we overlap the centre point of
for (const reroute of reroutes.values()) { for (const reroute of reroutes.values()) {
if (isPointInRect(reroute.pos, this._bounding)) children.add(reroute) if (isPointInRect(reroute.pos, this.boundingRect)) children.add(reroute)
} }
// Move groups we wholly contain // Move groups we wholly contain
for (const group of groups) { for (const group of groups) {
if (containsRect(this._bounding, group._bounding)) children.add(group) if (containsRect(this.boundingRect, group.boundingRect))
children.add(group)
} }
groups.sort((a, b) => { groups.sort((a, b) => {

View File

@@ -413,7 +413,7 @@ export class LGraphNode
} }
/** @inheritdoc {@link renderArea} */ /** @inheritdoc {@link renderArea} */
#renderArea: Float32Array = new Float32Array(4) #renderArea: [number, number, number, number] = [0, 0, 0, 0]
/** /**
* Rect describing the node area, including shadows and any protrusions. * Rect describing the node area, including shadows and any protrusions.
* Determines if the node is visible. Calculated once at the start of every frame. * Determines if the node is visible. Calculated once at the start of every frame.
@@ -443,9 +443,9 @@ export class LGraphNode
} }
/** {@link pos} and {@link size} values are backed by this {@link Rect}. */ /** {@link pos} and {@link size} values are backed by this {@link Rect}. */
_posSize: Float32Array = new Float32Array(4) _posSize: [number, number, number, number] = [0, 0, 0, 0]
_pos: Point = this._posSize.subarray(0, 2) _pos: Point = [0, 0]
_size: Size = this._posSize.subarray(2, 4) _size: Size = [0, 0]
public get pos() { public get pos() {
return this._pos return this._pos
@@ -1653,7 +1653,7 @@ export class LGraphNode
inputs ? inputs.filter((input) => !isWidgetInputSlot(input)).length : 1, inputs ? inputs.filter((input) => !isWidgetInputSlot(input)).length : 1,
outputs ? outputs.length : 1 outputs ? outputs.length : 1
) )
const size = out || new Float32Array([0, 0]) const size = out || [0, 0]
rows = Math.max(rows, 1) rows = Math.max(rows, 1)
// although it should be graphcanvas.inner_text_font size // although it should be graphcanvas.inner_text_font size
const font_size = LiteGraph.NODE_TEXT_SIZE const font_size = LiteGraph.NODE_TEXT_SIZE
@@ -2004,13 +2004,13 @@ export class LGraphNode
/** /**
* returns the bounding of the object, used for rendering purposes * returns the bounding of the object, used for rendering purposes
* @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage * @param out {Rect?} [optional] a place to store the output, to free garbage
* @param includeExternal {boolean?} [optional] set to true to * @param includeExternal {boolean?} [optional] set to true to
* include the shadow and connection points in the bounding calculation * include the shadow and connection points in the bounding calculation
* @returns the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] * @returns the bounding box in format of [topleft_cornerx, topleft_cornery, width, height]
*/ */
getBounding(out?: Rect, includeExternal?: boolean): Rect { getBounding(out?: Rect, includeExternal?: boolean): Rect {
out ||= new Float32Array(4) out ||= [0, 0, 0, 0]
const rect = includeExternal ? this.renderArea : this.boundingRect const rect = includeExternal ? this.renderArea : this.boundingRect
out[0] = rect[0] out[0] = rect[0]
@@ -2031,7 +2031,10 @@ export class LGraphNode
this.onBounding?.(bounds) this.onBounding?.(bounds)
const renderArea = this.#renderArea const renderArea = this.#renderArea
renderArea.set(bounds) renderArea[0] = bounds[0]
renderArea[1] = bounds[1]
renderArea[2] = bounds[2]
renderArea[3] = bounds[3]
// 4 offset for collapsed node connection points // 4 offset for collapsed node connection points
renderArea[0] -= 4 renderArea[0] -= 4
renderArea[1] -= 4 renderArea[1] -= 4
@@ -3174,7 +3177,7 @@ export class LGraphNode
* @returns the position * @returns the position
*/ */
getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point { getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point {
out ||= new Float32Array(2) out ||= [0, 0]
const { const {
pos: [nodeX, nodeY], pos: [nodeX, nodeY],

View File

@@ -109,7 +109,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
data?: number | string | boolean | { toToolTip?(): string } data?: number | string | boolean | { toToolTip?(): string }
_data?: unknown _data?: unknown
/** Centre point of the link, calculated during render only - can be inaccurate */ /** Centre point of the link, calculated during render only - can be inaccurate */
_pos: Float32Array _pos: [number, number]
/** @todo Clean up - never implemented in comfy. */ /** @todo Clean up - never implemented in comfy. */
_last_time?: number _last_time?: number
/** The last canvas 2D path that was used to render this link */ /** The last canvas 2D path that was used to render this link */
@@ -171,7 +171,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
this._data = null this._data = null
// center // center
this._pos = new Float32Array(2) this._pos = [0, 0]
} }
/** @deprecated Use {@link LLink.create} */ /** @deprecated Use {@link LLink.create} */

View File

@@ -49,8 +49,6 @@ export class Reroute
return Reroute.radius + gap + Reroute.slotRadius return Reroute.radius + gap + Reroute.slotRadius
} }
#malloc = new Float32Array(8)
/** The network this reroute belongs to. Contains all valid links and reroutes. */ /** The network this reroute belongs to. Contains all valid links and reroutes. */
#network: WeakRef<LinkNetwork> #network: WeakRef<LinkNetwork>
@@ -73,7 +71,7 @@ export class Reroute
/** This property is only defined on the last reroute of a floating reroute chain (closest to input end). */ /** This property is only defined on the last reroute of a floating reroute chain (closest to input end). */
floating?: FloatingRerouteSlot floating?: FloatingRerouteSlot
#pos = this.#malloc.subarray(0, 2) #pos: [number, number] = [0, 0]
/** @inheritdoc */ /** @inheritdoc */
get pos(): Point { get pos(): Point {
return this.#pos return this.#pos
@@ -126,14 +124,14 @@ export class Reroute
sin: number = 0 sin: number = 0
/** Bezier curve control point for the "target" (input) side of the link */ /** Bezier curve control point for the "target" (input) side of the link */
controlPoint: Point = this.#malloc.subarray(4, 6) controlPoint: [number, number] = [0, 0]
/** @inheritdoc */ /** @inheritdoc */
path?: Path2D path?: Path2D
/** @inheritdoc */ /** @inheritdoc */
_centreAngle?: number _centreAngle?: number
/** @inheritdoc */ /** @inheritdoc */
_pos: Float32Array = this.#malloc.subarray(6, 8) _pos: [number, number] = [0, 0]
/** @inheritdoc */ /** @inheritdoc */
_dragging?: boolean _dragging?: boolean

View File

@@ -193,7 +193,7 @@ export interface LinkSegment {
/** The last canvas 2D path that was used to render this segment */ /** The last canvas 2D path that was used to render this segment */
path?: Path2D path?: Path2D
/** Centre point of the {@link path}. Calculated during render only - can be inaccurate */ /** Centre point of the {@link path}. Calculated during render only - can be inaccurate */
readonly _pos: Float32Array readonly _pos: [number, number]
/** /**
* Y-forward along the {@link path} from its centre point, in radians. * Y-forward along the {@link path} from its centre point, in radians.
* `undefined` if using circles for link centres. * `undefined` if using circles for link centres.
@@ -225,34 +225,45 @@ export interface IFoundSlot extends IInputOrOutput {
} }
/** A point represented as `[x, y]` co-ordinates */ /** A point represented as `[x, y]` co-ordinates */
export type Point = [x: number, y: number] | Float32Array | Float64Array export type Point =
| [x: number, y: number]
| Float32Array
| Float64Array
| number[]
/** A size represented as `[width, height]` */ /** A size represented as `[width, height]` */
export type Size = [width: number, height: number] | Float32Array | Float64Array export type Size =
| [width: number, height: number]
| Float32Array
| Float64Array
| number[]
/** A very firm array */ /** A very firm array */
type ArRect = [x: number, y: number, width: number, height: number] type ArRect = [x: number, y: number, width: number, height: number]
/** A rectangle starting at top-left coordinates `[x, y, width, height]` */ /** A rectangle starting at top-left coordinates `[x, y, width, height]` */
export type Rect = ArRect | Float32Array | Float64Array export type Rect = ArRect | Float32Array | Float64Array | number[]
/** A point represented as `[x, y]` co-ordinates that will not be modified */ /** A point represented as `[x, y]` co-ordinates that will not be modified */
export type ReadOnlyPoint = export type ReadOnlyPoint =
| readonly [x: number, y: number] | readonly [x: number, y: number]
| ReadOnlyTypedArray<Float32Array> | ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array> | ReadOnlyTypedArray<Float64Array>
| readonly number[]
/** A size represented as `[width, height]` that will not be modified */ /** A size represented as `[width, height]` that will not be modified */
export type ReadOnlySize = export type ReadOnlySize =
| readonly [width: number, height: number] | readonly [width: number, height: number]
| ReadOnlyTypedArray<Float32Array> | ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array> | ReadOnlyTypedArray<Float64Array>
| readonly number[]
/** A rectangle starting at top-left coordinates `[x, y, width, height]` that will not be modified */ /** A rectangle starting at top-left coordinates `[x, y, width, height]` that will not be modified */
export type ReadOnlyRect = export type ReadOnlyRect =
| readonly [x: number, y: number, width: number, height: number] | readonly [x: number, y: number, width: number, height: number]
| ReadOnlyTypedArray<Float32Array> | ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array> | ReadOnlyTypedArray<Float64Array>
| readonly number[]
type TypedArrays = type TypedArrays =
| Int8Array | Int8Array

View File

@@ -331,7 +331,12 @@ export function createBounds(
objects: Iterable<HasBoundingRect>, objects: Iterable<HasBoundingRect>,
padding: number = 10 padding: number = 10
): ReadOnlyRect | null { ): ReadOnlyRect | null {
const bounds = new Float32Array([Infinity, Infinity, -Infinity, -Infinity]) const bounds: [number, number, number, number] = [
Infinity,
Infinity,
-Infinity,
-Infinity
]
for (const obj of objects) { for (const obj of objects) {
const rect = obj.boundingRect const rect = obj.boundingRect

View File

@@ -45,7 +45,7 @@ export abstract class SubgraphSlot
return LiteGraph.NODE_SLOT_HEIGHT return LiteGraph.NODE_SLOT_HEIGHT
} }
readonly #pos: Point = new Float32Array(2) readonly #pos: Point = [0, 0]
readonly measurement: ConstrainedSize = new ConstrainedSize( readonly measurement: ConstrainedSize = new ConstrainedSize(
SubgraphSlot.defaultHeight, SubgraphSlot.defaultHeight,