Skip to content

feat: Add VectorValue for allocation-free vector math - #4023

Draft
spydon wants to merge 1 commit into
perf/notifying-vector2-lean-notifierfrom
feat/vector-value
Draft

feat: Add VectorValue for allocation-free vector math#4023
spydon wants to merge 1 commit into
perf/notifying-vector2-lean-notifierfrom
feat/vector-value

Conversation

@spydon

@spydon spydon commented Aug 28, 2026

Copy link
Copy Markdown
Member

Description

Stacked on #4022. Adds VectorValue, an immutable 2D vector that is free to create and combine, and a value property on every Vector2 that reads and writes its components as a VectorValue. Together they make vector expressions allocation-free while keeping Vector2/NotifyingVector2 as the observable, mutable storage that components expose today. Nothing existing changes; this is additive.

VectorValue is an extension type over Float64x2, which the Dart VM keeps unboxed in locals, arguments, return values and fields, so a whole expression stays in registers. Every member carries @pragma('vm:prefer-inline'); without inlining each operator would return a boxed value, so the pragmas are part of the design. It keeps full double precision (VectorValue(0.1, 0).x == 0.1; Vector2 stores floats).

Assigning to value goes through setValues, so a NotifyingVector2 notifies its listeners exactly once per assignment, version increments once, and Transform2D picks the change up as usual.

What it looks like

Today With this PR Allocates
position.add(velocity * dt); position.value += velocity * dt; 2 objects → 0
position.setFrom((size / 2)..add(offset)..addScaled(velocity, dt)); position.value = size.value / 2 + offset + velocity * dt; 2 objects → 0
final d = target - position..normalize(); final d = (target.value - position.value).normalized(); 2 objects → 0
position.addScaled(velocity, dt); unchanged, still the fastest form for this one case 0
size.addListener(_relayout);, size.x = 32;, position.setFrom(v); unchanged

velocity and offset above are VectorValue fields (final velocity = VectorValue(30, -20);); a stored Vector2 on the right-hand side of an operator needs .value (a.position.value - b.position.value), because Dart has no operator overloading by argument type. VectorValue also has the usual helpers (dot, cross, length, normalized, withLength, rotated, lerp, distanceTo, angleTo, clamp, floor, equals, closeTo, copyInto, toOffset, …), all documented in doc/flame/other/util.md.

Benchmarks

benchmark/notifying_vector2_benchmark.dart, 10 000 PositionComponents, flutter test on macOS arm64, on top of #4022:

Workload Time Per component Allocates
position.add(velocity * dt) 86 µs 8.6 ns yes
position.value += velocity * dt 50 µs 5.0 ns no
position.addScaled(velocity, dt) 56 µs 5.6 ns no
position.setFrom((size / 2)..add(offset)..addScaled(velocity, dt)) 91 µs 9.1 ns yes
position.value = size.value / 2 + offset + velocity * dt 65 µs 6.5 ns no

For reference, on main the position.add(velocity * dt) row measures 331 µs, so the readable form is now the fast form. Standalone AOT measurements of the same shapes (dart compile exe, minimum of 21 samples) put the .value expression at 6.5-8 ns per component against 17 ns for today's NotifyingVector2 chain, and confirm that the Float64x2 temporaries do not reach the heap.

Caveats

  • Extension types cannot declare ==, hashCode or toString, and Float64x2 compares by identity, so VectorValue(1, 2) == VectorValue(1, 2) is false. Use equals/closeTo; describe() gives a readable string; do not use it as a map key. Worth a lint in flame_lint later.
  • Float64x2 has no const constructor, so VectorValue.zero/one are static final and it cannot be a default parameter value.
  • On the web Float64x2 is emulated; measured with dart2js it is still the fastest ergonomic option (8 ns for the full expression versus 22 ns for the Vector2 chain) as long as values are not stored in fields, which Vector2 storage guarantees.

Checklist

  • I have followed the Contributor Guide when preparing my PR.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • [-] I have updated/added relevant examples in examples or docs.

Breaking Change?

  • Yes, this PR is a breaking change.
  • No, this PR is not a breaking change.

Related Issues

Part of #3966. Stacked on #4022.

https://claude.ai/code/session_01PixXaRTPs8v1P2mQF2QHGG

Add VectorValue, an immutable 2D vector backed by Float64x2 that the VM
keeps unboxed, and a value property on Vector2 that reads and writes the
vector as a VectorValue, so that expressions such as
`position.value = size.value / 2 + offset + velocity * dt` run without
allocating while Vector2 and NotifyingVector2 remain the observable
storage.

Claude-Session: https://claude.ai/code/session_01PixXaRTPs8v1P2mQF2QHGG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant