Skip to content

Commit

Permalink
Use client ICU data with skwasm. (flutter#42018)
Browse files Browse the repository at this point in the history
This implements flutter/flutter#126340

For now, I am keeping this implementation simple without any extra caching. I may add caching later based on profiling results.
  • Loading branch information
eyebrowsoffire authored May 16, 2023
1 parent 1c775e3 commit 2cf50e7
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 47 deletions.
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 @@ -201,7 +201,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 @@ -210,7 +210,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 @@ -592,8 +597,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,
);
Loading

0 comments on commit 2cf50e7

Please sign in to comment.