Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use client ICU data with skwasm. #42018

Merged
merged 5 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/canvaskit/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Future<Uint8List> fetchImage(String url, WebOnlyImageCodecChunkCallback? chunkCa
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
Future<Uint8List> readChunked(HttpFetchPayload payload, int contentLength, WebOnlyImageCodecChunkCallback chunkCallback) async {
final JSUint8Array1 result = createUint8ArrayFromLength(contentLength);
final JSUint8Array result = createUint8ArrayFromLength(contentLength);
int position = 0;
int cumulativeBytesLoaded = 0;
await payload.read<JSUint8Array1>((JSUint8Array1 chunk) {
Expand All @@ -212,7 +212,7 @@ Future<Uint8List> readChunked(HttpFetchPayload payload, int contentLength, WebOn
result.set(chunk, position.toJS);
position += chunk.length.toDart.toInt();
});
return (result as JSUint8Array).toDart;
return result.toDart;
}

/// A [ui.Image] backed by an `SkImage` from Skia.
Expand Down
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,13 @@ Future<ByteBuffer> readVideoFramePixelsUnmodified(VideoFrame videoFrame) async {

// In dart2wasm, Uint8List is not the same as a JS Uint8Array. So we
// explicitly construct the JS object here.
final JSUint8Array1 destination = createUint8ArrayFromLength(size);
final JSUint8Array destination = createUint8ArrayFromLength(size);
final JsPromise copyPromise = videoFrame.copyTo(destination);
await promiseToFuture<void>(copyPromise);

// In dart2wasm, `toDart` incurs a copy here. On JS backends, this is a
// no-op.
return (destination as JSUint8Array).toDart.buffer;
return destination.toDart.buffer;
}

Future<Uint8List> encodeVideoFrameAsPng(VideoFrame videoFrame) async {
Expand Down
3 changes: 2 additions & 1 deletion lib/web_ui/lib/src/engine/canvaskit/text_fragmenter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:js_interop';
import 'dart:typed_data';

import '../dom.dart';
Expand Down Expand Up @@ -153,7 +154,7 @@ final DomV8BreakIterator _v8LineBreaker = createV8BreakIterator();

Uint32List fragmentUsingV8LineBreaker(String text) {
final List<LineBreakFragment> fragments =
breakLinesUsingV8BreakIterator(text, _v8LineBreaker);
breakLinesUsingV8BreakIterator(text, text.toJS, _v8LineBreaker);

final int size = (fragments.length + 1) * 2;
final Uint32List typedArray = Uint32List(size);
Expand Down
17 changes: 13 additions & 4 deletions lib/web_ui/lib/src/engine/dom.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3298,8 +3298,8 @@ class DomSegmenter {

extension DomSegmenterExtension on DomSegmenter {
@JS('segment')
external DomSegments _segment(JSString text);
DomSegments segment(String text) => _segment(text.toJS);
external DomSegments segmentRaw(JSString text);
DomSegments segment(String text) => segmentRaw(text.toJS);
}

@JS()
Expand Down Expand Up @@ -3398,8 +3398,7 @@ class DomV8BreakIterator {

extension DomV8BreakIteratorExtension on DomV8BreakIterator {
@JS('adoptText')
external JSVoid _adoptText(JSString text);
void adoptText(String text) => _adoptText(text.toJS);
external JSVoid adoptText(JSString text);

@JS('first')
external JSNumber _first();
Expand All @@ -3426,3 +3425,13 @@ DomV8BreakIterator createV8BreakIterator() {
return DomV8BreakIterator(
<JSAny?>[].toJS, const <String, String>{'type': 'line'}.toJSAnyDeep);
}

@JS('TextDecoder')
@staticInterop
class DomTextDecoder {
external factory DomTextDecoder();
}

extension DomTextDecoderExtension on DomTextDecoder {
external JSString decode(JSTypedArray buffer);
}
41 changes: 24 additions & 17 deletions lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,34 @@

import 'dart:js_interop';

@JS()
@staticInterop
class ArrayBuffer {}

@JS()
@staticInterop
class TypedArray {}

extension TypedArrayExtension on TypedArray {
external void set(JSUint8Array1 source, JSNumber start);
// Some APIs we need on typed arrays that are not exposed by the dart sdk yet
extension TypedArrayExtension on JSTypedArray {
external JSTypedArray slice(JSNumber start, JSNumber end);
external void set(JSTypedArray source, JSNumber start);
external JSNumber get length;
}

// Due to some differences between wasm and JS backends, we can't use the
// JSUint8Array object provided by the dart sdk. So for now, we can define this
// as an opaque JS object.
// These are constructors on `Uint8Array` that we need that aren't exposed in
// the dart sdk yet
@JS('Uint8Array')
@staticInterop
class JSUint8Array1 extends TypedArray {
external factory JSUint8Array1._(JSAny bufferOrLength);
class JSUint8Array1 extends JSTypedArray {
external factory JSUint8Array1._create1(JSAny bufferOrLength);
external factory JSUint8Array1._create3(
JSArrayBuffer buffer,
JSNumber start,
JSNumber length,
);
}

JSUint8Array1 createUint8ArrayFromBuffer(ArrayBuffer buffer) => JSUint8Array1._(buffer as JSObject);
JSUint8Array1 createUint8ArrayFromLength(int length) => JSUint8Array1._(length.toJS);
JSUint8Array createUint8ArrayFromBuffer(JSArrayBuffer buffer)
=> JSUint8Array1._create1(buffer) as JSUint8Array;

JSUint8Array createUint8ArrayFromSubBuffer(
JSArrayBuffer buffer,
int start,
int length,
) => JSUint8Array1._create3(buffer, start.toJS, length.toJS) as JSUint8Array;

JSUint8Array createUint8ArrayFromLength(int length)
=> JSUint8Array1._create1(length.toJS) as JSUint8Array;
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,15 @@ class SkwasmFontCollection implements FlutterFontCollection {
return FontNotFoundError(assetManager.getAssetUrl(asset.asset));
}
int length = 0;
final List<JSUint8Array1> chunks = <JSUint8Array1>[];
await response.read((JSUint8Array1 chunk) {
final List<JSUint8Array> chunks = <JSUint8Array>[];
await response.read((JSUint8Array chunk) {
length += chunk.length.toDart.toInt();
chunks.add(chunk);
});
final SkDataHandle fontData = skDataCreate(length);
int dataAddress = skDataGetPointer(fontData).cast<Int8>().address;
final JSUint8Array1 wasmMemory = createUint8ArrayFromBuffer(skwasmInstance.wasmMemory.buffer);
for (final JSUint8Array1 chunk in chunks) {
final JSUint8Array wasmMemory = createUint8ArrayFromBuffer(skwasmInstance.wasmMemory.buffer);
for (final JSUint8Array chunk in chunks) {
wasmMemory.set(chunk, dataAddress.toJS);
dataAddress += chunk.length.toDart.toInt();
}
Expand All @@ -138,15 +138,15 @@ class SkwasmFontCollection implements FlutterFontCollection {
Future<bool> loadFontFromUrl(String familyName, String url) async {
final HttpFetchResponse response = await httpFetch(url);
int length = 0;
final List<JSUint8Array1> chunks = <JSUint8Array1>[];
await response.read((JSUint8Array1 chunk) {
final List<JSUint8Array> chunks = <JSUint8Array>[];
await response.read((JSUint8Array chunk) {
length += chunk.length.toDart.toInt();
chunks.add(chunk);
});
final SkDataHandle fontData = skDataCreate(length);
int dataAddress = skDataGetPointer(fontData).cast<Int8>().address;
final JSUint8Array1 wasmMemory = createUint8ArrayFromBuffer(skwasmInstance.wasmMemory.buffer);
for (final JSUint8Array1 chunk in chunks) {
final JSUint8Array wasmMemory = createUint8ArrayFromBuffer(skwasmInstance.wasmMemory.buffer);
for (final JSUint8Array chunk in chunks) {
wasmMemory.set(chunk, dataAddress.toJS);
dataAddress += chunk.length.toDart.toInt();
}
Expand Down
93 changes: 93 additions & 0 deletions lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';
import 'dart:ffi';
import 'dart:js_interop';

import 'package:ui/src/engine.dart';
import 'package:ui/src/engine/skwasm/skwasm_impl.dart';
import 'package:ui/ui.dart' as ui;

const int _kSoftLineBreak = 0;
const int _kHardLineBreak = 100;

class SkwasmLineMetrics implements ui.LineMetrics {
factory SkwasmLineMetrics({
required bool hardBreak,
Expand Down Expand Up @@ -591,8 +596,96 @@ class SkwasmParagraphBuilder implements ui.ParagraphBuilder {
skString16Free(stringHandle);
}

static final DomV8BreakIterator _v8BreakIterator = createV8BreakIterator();
static final DomSegmenter _graphemeSegmenter = createIntlSegmenter(granularity: 'grapheme');
static final DomSegmenter _wordSegmenter = createIntlSegmenter(granularity: 'word');
static final DomTextDecoder _utf8Decoder = DomTextDecoder();

void _addSegmenterData() => withStackScope((StackScope scope) {
// Because some of the string processing is dealt with manually in dart,
// and some is handled by the browser, we actually need both a dart string
// and a JS string. Converting from linear memory to Dart strings or JS
// strings is more efficient than directly converting to each other, so we
// just create both up front here.
final Pointer<Uint32> outSize = scope.allocUint32Array(1);
final Pointer<Uint8> utf8Data = paragraphBuilderGetUtf8Text(handle, outSize);
if (utf8Data == nullptr) {
return;
}

// TODO(jacksongardner): We could make a subclass of `List<int>` here to
// avoid this copy.
final List<int> codeUnitList = List<int>.generate(
outSize.value,
(int index) => utf8Data[index]
);
final String text = utf8.decode(codeUnitList);
final JSString jsText = _utf8Decoder.decode(
// In an ideal world we would just use a subview of wasm memory rather
// than a slice, but the TextDecoder API doesn't work on shared buffer
// sources yet.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1012656
createUint8ArrayFromBuffer(skwasmInstance.wasmMemory.buffer).slice(
utf8Data.address.toJS,
(utf8Data.address + outSize.value).toJS
));

_addGraphemeBreakData(text, jsText);
_addWordBreakData(text, jsText);
_addLineBreakData(text, jsText);
});

UnicodePositionBufferHandle _createBreakPositionBuffer(String text, JSString jsText, DomSegmenter segmenter) {
final DomIteratorWrapper<DomSegment> iterator = segmenter.segmentRaw(jsText).iterator();
final List<int> breaks = <int>[];
while (iterator.moveNext()) {
breaks.add(iterator.current.index);
}
breaks.add(text.length);

final UnicodePositionBufferHandle positionBuffer = unicodePositionBufferCreate(breaks.length);
final Pointer<Uint32> buffer = unicodePositionBufferGetDataPointer(positionBuffer);
for (int i = 0; i < breaks.length; i++) {
buffer[i] = breaks[i];
}
return positionBuffer;
}

void _addGraphemeBreakData(String text, JSString jsText) {
final UnicodePositionBufferHandle positionBuffer =
_createBreakPositionBuffer(text, jsText, _graphemeSegmenter);
paragraphBuilderSetGraphemeBreaksUtf16(handle, positionBuffer);
unicodePositionBufferFree(positionBuffer);
}

void _addWordBreakData(String text, JSString jsText) {
final UnicodePositionBufferHandle positionBuffer =
_createBreakPositionBuffer(text, jsText, _wordSegmenter);
paragraphBuilderSetWordBreaksUtf16(handle, positionBuffer);
unicodePositionBufferFree(positionBuffer);
}

void _addLineBreakData(String text, JSString jsText) {
final List<LineBreakFragment> lineBreaks = breakLinesUsingV8BreakIterator(text, jsText, _v8BreakIterator);
final LineBreakBufferHandle lineBreakBuffer = lineBreakBufferCreate(lineBreaks.length + 1);
final Pointer<LineBreak> lineBreakPointer = lineBreakBufferGetDataPointer(lineBreakBuffer);

// First line break is always zero. The buffer is zero initialized, so we can just
// skip the first one.
for (int i = 0; i < lineBreaks.length; i++) {
final LineBreakFragment fragment = lineBreaks[i];
lineBreakPointer[i + 1].position = fragment.end;
lineBreakPointer[i + 1].lineBreakType = fragment.type == LineBreakType.mandatory
? _kHardLineBreak
: _kSoftLineBreak;
}
paragraphBuilderSetLineBreaksUtf16(handle, lineBreakBuffer);
lineBreakBufferFree(lineBreakBuffer);
}

@override
ui.Paragraph build() {
_addSegmenterData();
return SkwasmParagraph(paragraphBuilderBuild(handle));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@

import 'dart:js_interop';

import 'package:ui/src/engine.dart';

@JS()
@staticInterop
class WebAssemblyMemory {}

extension WebAssemblyMemoryExtension on WebAssemblyMemory {
external ArrayBuffer get buffer;
external JSArrayBuffer get buffer;
}

@JS()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ typedef ParagraphHandle = Pointer<RawParagraph>;
final class RawTextBoxList extends Opaque {}
typedef TextBoxListHandle = Pointer<RawTextBoxList>;

final class RawUnicodePositionBuffer extends Opaque {}
typedef UnicodePositionBufferHandle = Pointer<RawUnicodePositionBuffer>;

final class RawLineBreakBuffer extends Opaque {}
typedef LineBreakBufferHandle = Pointer<RawLineBreakBuffer>;

final class LineBreak extends Struct {
@Int32()
external int position;

@Int32()
external int lineBreakType;
}

@Native<Void Function(ParagraphHandle)>(symbol: 'paragraph_dispose', isLeaf: true)
external void paragraphDispose(ParagraphHandle handle);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ external void paragraphBuilderAddText(
SkString16Handle text,
);

@Native<Pointer<Uint8> Function(
ParagraphBuilderHandle,
Pointer<Uint32>
)>(symbol: 'paragraphBuilder_getUtf8Text', isLeaf: true)
external Pointer<Uint8> paragraphBuilderGetUtf8Text(
ParagraphBuilderHandle handle,
Pointer<Uint32> outSize
);

@Native<Void Function(
ParagraphBuilderHandle,
TextStyleHandle,
Expand All @@ -64,3 +73,48 @@ external void paragraphBuilderPop(ParagraphBuilderHandle handle);

@Native<ParagraphHandle Function(ParagraphBuilderHandle)>(symbol: 'paragraphBuilder_build', isLeaf: true)
external ParagraphHandle paragraphBuilderBuild(ParagraphBuilderHandle handle);

@Native<UnicodePositionBufferHandle Function(Size)>(
symbol: 'unicodePositionBuffer_create', isLeaf: true)
external UnicodePositionBufferHandle unicodePositionBufferCreate(int size);

@Native<Pointer<Uint32> Function(UnicodePositionBufferHandle)>(
symbol: 'unicodePositionBuffer_getDataPointer', isLeaf: true)
external Pointer<Uint32> unicodePositionBufferGetDataPointer(UnicodePositionBufferHandle handle);

@Native<Void Function(UnicodePositionBufferHandle)>(
symbol: 'unicodePositionBuffer_free', isLeaf: true)
external void unicodePositionBufferFree(UnicodePositionBufferHandle handle);

@Native<LineBreakBufferHandle Function(Size)>(
symbol: 'lineBreakBuffer_create', isLeaf: true)
external LineBreakBufferHandle lineBreakBufferCreate(int size);

@Native<Pointer<LineBreak> Function(LineBreakBufferHandle)>(
symbol: 'lineBreakBuffer_getDataPointer', isLeaf: true)
external Pointer<LineBreak> lineBreakBufferGetDataPointer(LineBreakBufferHandle handle);

@Native<Void Function(LineBreakBufferHandle)>(
symbol: 'lineBreakBuffer_free', isLeaf: true)
external void lineBreakBufferFree(LineBreakBufferHandle handle);

@Native<Void Function(ParagraphBuilderHandle, UnicodePositionBufferHandle)>(
symbol: 'paragraphBuilder_setGraphemeBreaksUtf16', isLeaf: true)
external void paragraphBuilderSetGraphemeBreaksUtf16(
ParagraphBuilderHandle handle,
UnicodePositionBufferHandle positionBuffer,
);

@Native<Void Function(ParagraphBuilderHandle, UnicodePositionBufferHandle)>(
symbol: 'paragraphBuilder_setWordBreaksUtf16', isLeaf: true)
external void paragraphBuilderSetWordBreaksUtf16(
ParagraphBuilderHandle handle,
UnicodePositionBufferHandle positionBuffer,
);

@Native<Void Function(ParagraphBuilderHandle, LineBreakBufferHandle)>(
symbol: 'paragraphBuilder_setLineBreaksUtf16', isLeaf: true)
external void paragraphBuilderSetLineBreaksUtf16(
ParagraphBuilderHandle handle,
LineBreakBufferHandle positionBuffer,
);