e0b028808a
Add more Wasm SIMD intrinsics and instructions to support efficient vector operations, specifically targeting the requirements of types like Offset and Size. - Implement new intrinsics in `dart2wasm`: - `anyTrue` for V128. - `allTrue` for I64x2. - `pmin`/`pmax` (pseudo-minimum/maximum) for F64x2. - `fromLaneValues` for F32x4 and F64x2. - `shuffle` for F64x2. - Update `wasm_builder` with support for the following instructions: - `v128.any_true` - `i8x16.all_true`, `i16x8.all_true`, `i32x4.all_true`, `i64x2.all_true` - `i8x16.shuffle` (including serialization and deserialization) - Add `tests/web/wasm/simd/vector_test.dart`, which implements `Offset` and `Size` using these new SIMD intrinsics, demonstrating their practical utility and correctness. - Update `tests/web/wasm/simd/simd_test.dart` with additional coverage. Change-Id: Ifa5ba0fb265b0fa46c0a3469d9331f32a025c9ec Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/478860 Commit-Queue: Kevin Moore <kevmoo@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com>
223 lines
6.4 KiB
Dart
223 lines
6.4 KiB
Dart
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
// dart2wasmOptions=--extra-compiler-option=--enable-experimental-wasm-interop
|
|
|
|
import 'package:expect/expect.dart';
|
|
|
|
// ignore: import_internal_library
|
|
import 'dart:_wasm';
|
|
import 'dart:math' as math;
|
|
|
|
// Flutter has an OffsetBase class that's extended by Offset and Size
|
|
// Let's implement those using f64x2 as the storage
|
|
|
|
abstract final class OffsetBase {
|
|
final WasmF64x2 _storage;
|
|
|
|
const OffsetBase(this._storage);
|
|
|
|
double get _dx => _storage.extractLane(0).toDouble();
|
|
double get _dy => _storage.extractLane(1).toDouble();
|
|
|
|
bool get isInfinite => _dx.isInfinite || _dy.isInfinite;
|
|
|
|
bool get isFinite => _dx.isFinite && _dy.isFinite;
|
|
|
|
bool operator <(OffsetBase other) => _storage.lt(other._storage).allTrue;
|
|
|
|
bool operator <=(OffsetBase other) => _storage.le(other._storage).allTrue;
|
|
|
|
bool operator >(OffsetBase other) => _storage.gt(other._storage).allTrue;
|
|
|
|
bool operator >=(OffsetBase other) => _storage.ge(other._storage).allTrue;
|
|
|
|
@override
|
|
int get hashCode => Object.hash(_dx, _dy);
|
|
}
|
|
|
|
final class Offset extends OffsetBase {
|
|
const Offset(super.storage);
|
|
|
|
factory Offset.from(double dx, double dy) {
|
|
return Offset(WasmF64x2.fromLaneValues(dx.toWasmF64(), dy.toWasmF64()));
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
return other is Offset && other._storage.eq(_storage).allTrue;
|
|
}
|
|
|
|
double get dx => _dx;
|
|
double get dy => _dy;
|
|
|
|
Offset operator +(Offset other) => Offset(_storage + other._storage);
|
|
|
|
Offset operator -(Offset other) => Offset(_storage - other._storage);
|
|
|
|
Offset operator *(double operand) =>
|
|
Offset(_storage * WasmF64x2.splat(operand.toWasmF64()));
|
|
|
|
Offset operator /(double operand) =>
|
|
Offset(_storage / WasmF64x2.splat(operand.toWasmF64()));
|
|
|
|
double get distance => _distanceSquaredWasm.sqrt().toDouble();
|
|
|
|
double get distanceSquared => _distanceSquaredWasm.toDouble();
|
|
|
|
WasmF64 get _distanceSquaredWasm {
|
|
final squares = _storage * _storage;
|
|
return (squares + squares.shuffle(squares, const [1, 0])).extractLane(0);
|
|
}
|
|
|
|
double get direction => math.atan2(_dy, _dx);
|
|
|
|
Offset scale(double scaleX, double scaleY) => Offset(
|
|
_storage * WasmF64x2.fromLaneValues(scaleX.toWasmF64(), scaleY.toWasmF64()),
|
|
);
|
|
|
|
Offset translate(double translateX, double translateY) => Offset(
|
|
_storage +
|
|
WasmF64x2.fromLaneValues(
|
|
translateX.toWasmF64(),
|
|
translateY.toWasmF64(),
|
|
),
|
|
);
|
|
|
|
Offset operator -() => Offset(-_storage);
|
|
|
|
static Offset? lerp(Offset? a, Offset? b, double t) {
|
|
if (a == null && b == null) return null;
|
|
if (a == null) return b! * t;
|
|
if (b == null) return a * (1.0 - t);
|
|
return Offset(
|
|
(a._storage * WasmF64x2.splat((1.0 - t).toWasmF64())) +
|
|
(b._storage * WasmF64x2.splat(t.toWasmF64())),
|
|
);
|
|
}
|
|
|
|
@override
|
|
String toString() =>
|
|
'Offset(${dx.toStringAsFixed(1)}, ${dy.toStringAsFixed(1)})';
|
|
}
|
|
|
|
final class Size extends OffsetBase {
|
|
const Size._(super.storage);
|
|
|
|
factory Size(double width, double height) {
|
|
return Size._(
|
|
WasmF64x2.fromLaneValues(width.toWasmF64(), height.toWasmF64()),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
return other is Size && other._storage.eq(_storage).allTrue;
|
|
}
|
|
|
|
static Size copy(Size source) => Size(source.width, source.height);
|
|
|
|
static Size square(double dimension) => Size(dimension, dimension);
|
|
|
|
static Size get zero => Size._(_zeroF64x2);
|
|
|
|
double get width => _dx;
|
|
double get height => _dy;
|
|
|
|
bool get isEmpty => _storage.le(_zeroF64x2).anyTrue;
|
|
|
|
double get aspectRatio {
|
|
var swapped = _storage.shuffle(_storage, const [1, 0]); // [h, w]
|
|
var div = _storage / swapped; // [w/h, h/w]
|
|
var mask = swapped.eq(_zeroF64x2); // [h==0, w==0]
|
|
// Select 0.0 if mask is true (h==0), else div.
|
|
var res = WasmF64x2(mask.bitSelect(_zeroF64x2, div));
|
|
return res.extractLane(0).toDouble();
|
|
}
|
|
|
|
double get shortestSide => _storage
|
|
.pmin(_storage.shuffle(_storage, const [1, 0]))
|
|
.extractLane(0)
|
|
.toDouble();
|
|
|
|
double get longestSide => _storage
|
|
.pmax(_storage.shuffle(_storage, const [1, 0]))
|
|
.extractLane(0)
|
|
.toDouble();
|
|
|
|
Size get center =>
|
|
Size._(_storage * WasmF64x2.splat(WasmF64.fromDouble(0.5)));
|
|
|
|
bool contains(Offset offset) {
|
|
return WasmI64x2(
|
|
offset._storage.ge(_zeroF64x2) & offset._storage.lt(_storage),
|
|
).allTrue;
|
|
}
|
|
|
|
@override
|
|
String toString() =>
|
|
'Size(${width.toStringAsFixed(1)}, ${height.toStringAsFixed(1)})';
|
|
}
|
|
|
|
void main() {
|
|
final off1 = Offset.from(10.0, 20.0);
|
|
final off2 = Offset.from(5.0, 5.0);
|
|
|
|
print('Offset1: $off1');
|
|
print('Offset2: $off2');
|
|
|
|
final sum = off1 + off2;
|
|
print('Sum: $sum');
|
|
Expect.equals(15.0, sum.dx);
|
|
Expect.equals(25.0, sum.dy);
|
|
|
|
final sub = off1 - off2;
|
|
print('Sub: $sub');
|
|
Expect.equals(5.0, sub.dx);
|
|
Expect.equals(15.0, sub.dy);
|
|
|
|
final scaled = off1 * 2.0;
|
|
print('Scaled: $scaled');
|
|
Expect.equals(20.0, scaled.dx);
|
|
Expect.equals(40.0, scaled.dy);
|
|
|
|
final div = off1 / 2.0;
|
|
print('Div: $div');
|
|
Expect.equals(5.0, div.dx);
|
|
Expect.equals(10.0, div.dy);
|
|
|
|
// New Offset validations
|
|
Expect.isTrue(off1 > off2);
|
|
Expect.isFalse(off1 < off2);
|
|
Expect.isTrue(sum == Offset.from(15.0, 25.0));
|
|
Expect.isTrue(Offset.from(3.0, 4.0).distance == 5.0);
|
|
Expect.equals(Offset.from(10.0, 0.0).direction, 0.0);
|
|
Expect.equals(Offset.from(0.0, 10.0).direction, math.pi / 2);
|
|
|
|
final size = Size(100.0, 200.0);
|
|
print('Size: $size');
|
|
Expect.equals(100.0, size.width);
|
|
Expect.equals(200.0, size.height);
|
|
Expect.isFalse(size.isEmpty);
|
|
Expect.equals(0.5, size.aspectRatio);
|
|
Expect.equals(100.0, size.shortestSide);
|
|
Expect.equals(200.0, size.longestSide);
|
|
Expect.isTrue(size.contains(Offset.from(50.0, 50.0)));
|
|
Expect.isFalse(size.contains(Offset.from(150.0, 50.0)));
|
|
Expect.equals(size.center, Size(50.0, 100.0));
|
|
|
|
// Test valid aspectRatio
|
|
Expect.equals(0.5, size.aspectRatio);
|
|
|
|
// Test zero height aspectRatio
|
|
final zeroHeight = Size(100.0, 0.0);
|
|
Expect.equals(0.0, zeroHeight.aspectRatio);
|
|
|
|
// Test zero width aspectRatio (should be infinite, though unrelated to height check logic)
|
|
final zeroWidth = Size(0.0, 100.0);
|
|
Expect.equals(0.0, zeroWidth.aspectRatio); // 0.0 / 100.0 = 0.0
|
|
}
|
|
|
|
WasmF64x2 get _zeroF64x2 => WasmF64x2.splat(WasmF64.fromDouble(0.0));
|