blob: ebdcab6bcb1f8ef674b6590ca5f7da7bec106aef [file]
/*
* Copyright 2022 Rive
*/
#include "testing_window.hpp"
#if !defined(RIVE_CANVAS_2D)
TestingWindow* TestingWindow::MakeCanvas2D() { return nullptr; }
#else
#include "utils/factory_utils.hpp"
#include <string>
#include <vector>
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#include <emscripten/val.h>
namespace rive::gpu
{
EM_ASYNC_JS(void, testingWindowCanvas2dImportCanvasAdvanced, (), {
// clang-format off
if (globalThis.canvasAdvancedModule === undefined)
{
const result = await import("./canvas_advanced.mjs");
globalThis.canvasAdvancedModule = await result.default();
}
// clang-format on
});
// Resets the canvas and fills it with `color` (0xAARRGGBB)
EM_JS(void, testingWindowCanvas2dResetCanvas, (uint32_t color), {
var canvas = document["getElementById"]("canvas");
var ctx = canvas["getContext"]("2d");
ctx["reset"]();
// reset() leaves the canvas transparent, so compositing the clear color
// over it yields that color, alpha included.
ctx["fillStyle"] = "rgba(" + ((0x00ff0000 & color) >>> 16) + "," +
((0x0000ff00 & color) >>> 8) + "," +
((0x000000ff & color) >>> 0) + "," +
((0xff000000 & color) >>> 24) / 0xff + ")";
ctx["fillRect"](0, 0, canvas["width"], canvas["height"]);
});
EM_JS(
bool,
testingWindowCanvas2dGetCanvasPixels,
(uint8_t* outBuffer, int width, int height),
{
var canvas = document["getElementById"]("canvas");
var ctx = canvas["getContext"]("2d");
var imageData = ctx["getImageData"](0, 0, canvas.width, canvas.height);
var pixelArray = imageData["data"];
if (pixelArray.length != width * height * 4)
{
return false;
}
// TestHarness::savePNG() reads this buffer bottom-up, the GL
// convention every other backend fills it with. getImageData()
// returns rows top-down, so reverse them here.
var stride = width * 4;
for (var y = 0; y < height; ++y)
{
Module['HEAPU8']['set'](
pixelArray['subarray'](
(height - 1 - y) * stride,
(height - y) * stride),
outBuffer + y * stride);
}
return true;
});
// Image decoding is asynchronous. renderer.js hands the bytes to an <img>
// element through a blob URL and fills in the result on its load event, so this
// awaits that event and returns the image as a value handle.
//
// Module.decodeImage is the entry point that works here.
// renderFactory.makeRenderImage()'s onDecode callback closes over renderer.js's
// loadContext, which is null outside Module.load(), and this harness draws
// images without ever loading a .riv file.
EM_ASYNC_JS(
emscripten::EM_VAL,
testingWindowCanvas2dDecodeImage,
(const uint8_t* bytes, int size),
{
// Defensive copy out of this module's heap; renderer.js's
// `decode` copies the bytes into a Blob as well.
var copy = Module['HEAPU8']['slice'](bytes, bytes + size);
var image = await new Promise(function(resolve) {
globalThis.canvasAdvancedModule["decodeImage"](copy, resolve);
});
return Emval.toHandle(image);
});
// Resolved on first use because the module is not on globalThis until
// testingWindowCanvas2dImportCanvasAdvanced() has finished.
static emscripten::val& canvasAdvancedModule()
{
static emscripten::val module = emscripten::val::global(
"canvasAdvancedModule");
return module;
}
// embind exposes C++ enums to JS as value objects, and renderer.js compares
// them by identity, so calls into it have to carry canvas_advanced's own
// objects. This class maps a C++ enumerator to the matching object, reading the
// table embind stores on the enum's JS constructor as `.values[rawValue]` (see
// _embind_register_enum_value in libembind.js).
class JSEnum
{
public:
// Must not be constructed until canvas_advanced.mjs has finished loading.
explicit JSEnum(const char* enumName)
{
emscripten::val values = canvasAdvancedModule()[enumName]["values"];
assert(!values.isUndefined()); // Not an embind enum?
// Indexed by raw value. These enums are sparse, so the gaps stay
// undefined and operator() asserts on them.
emscripten::val keys = emscripten::val::global("Object")
.call<emscripten::val>("keys", values);
size_t keyCount = keys["length"].as<size_t>();
for (size_t i = 0; i < keyCount; i++)
{
int rawValue = std::stoi(keys[i].as<std::string>());
assert(rawValue >= 0);
// m_values is a dense vector, so an enum with large raw values,
// such as a bit-mask, would make it huge.
assert(rawValue < 1024);
if (static_cast<size_t>(rawValue) >= m_values.size())
{
m_values.resize(rawValue + 1, emscripten::val::undefined());
}
m_values[rawValue] = values[rawValue];
}
}
template <typename T> const emscripten::val& operator()(T value) const
{
size_t rawValue = static_cast<size_t>(value);
assert(rawValue < m_values.size());
assert(!m_values[rawValue].isUndefined());
return m_values[rawValue];
}
private:
std::vector<emscripten::val> m_values;
};
// These strings are the names canvas_advanced registers in
// EMSCRIPTEN_BINDINGS(RiveWASM_C2D), which need not match the C++ type names.
struct JSEnums
{
JSEnum blendMode{"BlendMode"};
JSEnum fillRule{"FillRule"};
JSEnum paintStyle{"RenderPaintStyle"};
JSEnum strokeCap{"StrokeCap"};
JSEnum strokeJoin{"StrokeJoin"};
JSEnum strokePosition{"StrokePosition"};
JSEnum imageFilter{"ImageFilter"};
JSEnum imageWrap{"ImageWrap"};
};
// Built on first use, which must come after the
// testingWindowCanvas2dImportCanvasAdvanced() call in TestingWindowCanvas2D's
// constructor.
static const JSEnums& jsEnums()
{
static JSEnums enums;
return enums;
}
// The classes below drive the canvas2d renderer that lives in
// canvas_advanced.mjs.
//
// This harness and canvas_advanced are separately linked Emscripten modules,
// each with its own WebAssembly.Memory, C++ runtime and embind type registry. A
// pointer is an offset into one module's linear memory, so nothing that is or
// contains one -- a rive::Renderer*, an rcp<>, a vtable -- can be handed
// across. JS objects can, since there is one JavaScript realm; that includes
// TypedArrays, which are ordinary JS objects that view a particular module's
// heap.
//
// So each class holds the emscripten::val of its canvas_advanced counterpart
// and forwards to it, either directly (m_jsPath.call<void>("moveTo", x, y)) or
// through Canvas2DTestUtilities, which wraps the calls whose logic the harness
// would otherwise have to duplicate.
//
// The JS handle stays the sole owner of each object, and embind never reclaims
// raw pointer handles by itself, so every destructor here releases explicitly.
// It calls deleteLater(), since renderer.js captures paths and paints in
// deferred draw-list closures that must outlive the C++ object until endFrame()
// flushes them.
//
// Every renderer object here originates from JSRenderFactory, so a failed
// downcast is a bug; hence lite_rtti_cast_or_assert<> over the silent
// LITE_RTTI_CAST_OR_* macros.
// Gradients have no JS-side object to wrap, so there is no handle for
// makeLinearGradient() to return. These hold the parameters and replay them
// onto the paint from JSRenderPaint::shader(), mirroring Canvas2DGradientShader
// in bindings_c2d.cpp.
class JSGradientShader
: public LITE_RTTI_OVERRIDE(RenderShader, JSGradientShader)
{
public:
JSGradientShader(const ColorInt colors[], const float stops[], size_t count)
: m_stops(stops, stops + count)
, m_colors(colors, colors + count)
{}
virtual void applyToPaint(const emscripten::val& jsPaint) const = 0;
protected:
void applyStopsToPaint(const emscripten::val& jsPaint) const
{
for (size_t i = 0; i < m_stops.size(); ++i)
{
jsPaint.call<void>("addStop", m_colors[i], m_stops[i]);
}
}
private:
std::vector<float> m_stops;
std::vector<ColorInt> m_colors;
};
class JSLinearGradientShader : public JSGradientShader
{
public:
JSLinearGradientShader(
const ColorInt colors[],
const float stops[],
size_t count,
float sx,
float sy,
float ex,
float ey)
: JSGradientShader(colors, stops, count)
, m_startX(sx)
, m_startY(sy)
, m_endX(ex)
, m_endY(ey)
{}
void applyToPaint(const emscripten::val& jsPaint) const override
{
jsPaint.call<void>("linearGradient", m_startX, m_startY, m_endX, m_endY);
applyStopsToPaint(jsPaint);
}
private:
float m_startX;
float m_startY;
float m_endX;
float m_endY;
};
class JSRadialGradientShader : public JSGradientShader
{
public:
JSRadialGradientShader(
const ColorInt colors[],
const float stops[],
size_t count,
float cx,
float cy,
float radius)
: JSGradientShader(colors, stops, count)
, m_centerX(cx)
, m_centerY(cy)
, m_radius(radius)
{}
void applyToPaint(const emscripten::val& jsPaint) const override
{
// renderer.js takes the radius as a second point on the circle. Matches
// Canvas2DRadialGradientShader::passToJS in bindings_c2d.cpp.
jsPaint.call<void>(
"radialGradient",
m_centerX,
m_centerY,
m_centerX + m_radius,
m_centerY);
applyStopsToPaint(jsPaint);
}
private:
float m_centerX;
float m_centerY;
float m_radius;
};
class JSAngularGradientShader : public JSGradientShader
{
public:
JSAngularGradientShader(
const ColorInt colors[],
const float stops[],
size_t count)
: JSGradientShader(colors, stops, count)
{}
void applyToPaint(const emscripten::val& jsPaint) const override
{
jsPaint.call<void>("angularGradient");
applyStopsToPaint(jsPaint);
}
};
class JSRenderPath : public LITE_RTTI_OVERRIDE(RenderPath, JSRenderPath)
{
public:
JSRenderPath(emscripten::val&& jsPath)
: m_jsPath(std::forward<emscripten::val>(jsPath))
{}
// renderer.js's _drawPath/_clipPath capture the path in deferred draw-list
// closures, so its deletion waits for the flush in
// TestingWindowCanvas2D::endFrame().
~JSRenderPath() override { m_jsPath.call<void>("deleteLater"); }
void rewind() override { m_jsPath.call<void>("rewind"); }
void fillRule(FillRule value) override
{
m_jsPath.call<void>("fillRule", jsEnums().fillRule(value));
}
void moveTo(float x, float y) override
{
m_jsPath.call<void>("moveTo", x, y);
}
void lineTo(float x, float y) override
{
m_jsPath.call<void>("lineTo", x, y);
}
void cubicTo(float ox, float oy, float ix, float iy, float x, float y)
override
{
m_jsPath.call<void>("cubicTo", ox, oy, ix, iy, x, y);
}
void close() override { m_jsPath.call<void>("close"); }
void addRenderPath(const RenderPath* path, const Mat2D& transform) override
{
float xx = transform.xx();
float xy = transform.xy();
float yx = transform.yx();
float yy = transform.yy();
float tx = transform.tx();
float ty = transform.ty();
auto* jsPath = rive::lite_rtti_cast_or_assert<const JSRenderPath*>(path);
m_jsPath.call<void>("addPath", jsPath->js(), xx, xy, yx, yy, tx, ty);
}
void addRawPath(const RawPath& path) override
{
const rive::Vec2D* pts = path.points().data();
for (auto v : path.verbs())
{
switch ((rive::PathVerb)v)
{
case rive::PathVerb::move:
move(*pts++);
break;
case rive::PathVerb::line:
line(*pts++);
break;
case rive::PathVerb::cubic:
cubic(pts[0], pts[1], pts[2]);
pts += 3;
break;
case rive::PathVerb::close:
close();
break;
default:
assert(false); // unexpected verb
}
}
assert(pts - path.points().data() == path.points().size());
}
emscripten::val& js() { return m_jsPath; }
const emscripten::val& js() const { return m_jsPath; }
private:
emscripten::val m_jsPath;
};
class JSRenderImage : public LITE_RTTI_OVERRIDE(RenderImage, JSRenderImage)
{
public:
JSRenderImage(emscripten::val&& jsImage)
: m_jsImage(std::forward<emscripten::val>(jsImage))
{
// On load, renderer.js calls size() on the RenderImage it created,
// setting the dimensions on the wrapper in canvas_advanced's heap. This
// is a separate rive::RenderImage with its own m_Width/m_Height, which
// rive core reads, so copy them across.
emscripten::val testUtils = canvasAdvancedModule()["Canvas2DTestUtilities"];
m_Width = testUtils.call<int>("imageWidth", m_jsImage);
m_Height = testUtils.call<int>("imageHeight", m_jsImage);
}
~JSRenderImage() override { m_jsImage.call<void>("deleteLater"); }
emscripten::val& js() { return m_jsImage; }
const emscripten::val& js() const { return m_jsImage; }
private:
emscripten::val m_jsImage;
};
class JSRenderPaint : public LITE_RTTI_OVERRIDE(RenderPaint, JSRenderPaint)
{
public:
JSRenderPaint(emscripten::val&& jsPaint)
: m_jsPaint(std::forward<emscripten::val>(jsPaint))
{}
~JSRenderPaint() override { m_jsPaint.call<void>("deleteLater"); }
void style(RenderPaintStyle style) override
{
m_jsPaint.call<void>("style", jsEnums().paintStyle(style));
}
void color(ColorInt value) override { m_jsPaint.call<void>("color", value); }
void thickness(float value) override
{
m_jsPaint.call<void>("thickness", value);
}
void join(StrokeJoin value) override
{
m_jsPaint.call<void>("join", jsEnums().strokeJoin(value));
}
void cap(StrokeCap value) override
{
m_jsPaint.call<void>("cap", jsEnums().strokeCap(value));
}
void strokePosition(StrokePosition value) override
{
m_jsPaint.call<void>("strokePosition", jsEnums().strokePosition(value));
}
void feather(float value) override { m_jsPaint.call<void>("feather", value); }
void blendMode(BlendMode value) override
{
m_jsPaint.call<void>("blendMode", jsEnums().blendMode(value));
}
void shader(rcp<RenderShader> shader) override
{
if (shader == nullptr)
{
m_jsPaint.call<void>("clearGradient");
return;
}
rive::lite_rtti_cast_or_assert<JSGradientShader*>(shader.get())
->applyToPaint(m_jsPaint);
}
void shaderTransform(const Mat2D& transform) override
{
m_jsPaint.call<void>(
"gradientTransform",
transform.xx(),
transform.xy(),
transform.yx(),
transform.yy(),
transform.tx(),
transform.ty());
}
void invalidateStroke() override {}
void modulatedImage(
const RenderImage* image,
ImageSampler sampler,
const Mat2D& xform) override
{
auto* jsImage = rive::lite_rtti_cast_or_assert<const JSRenderImage*>(image);
m_jsPaint.call<void>(
"modulatedImage",
jsImage->js(),
jsEnums().imageFilter(sampler.filter),
jsEnums().imageWrap(sampler.wrapX),
jsEnums().imageWrap(sampler.wrapY),
xform[0],
xform[1],
xform[2],
xform[3],
xform[4],
xform[5]);
}
emscripten::val& js() { return m_jsPaint; }
const emscripten::val& js() const { return m_jsPaint; }
private:
emscripten::val m_jsPaint;
};
class JSRenderFactory : public rive::Factory
{
public:
JSRenderFactory() : m_jsFactory(canvasAdvancedModule()["renderFactory"]) {}
rcp<RenderBuffer> makeRenderBuffer(
RenderBufferType type,
RenderBufferFlags flags,
size_t sizeInBytes) override
{
// These never cross the module boundary: rive core maps and fills them
// here, and JSRenderer::drawImageMesh() copies the contents over when
// it hands off.
return make_rcp<DataRenderBuffer>(type, flags, sizeInBytes);
}
rcp<RenderShader> makeLinearGradient(
float sx,
float sy,
float ex,
float ey,
const ColorInt colors[],
const float stops[],
size_t count) override
{
return rcp<RenderShader>(
new JSLinearGradientShader(colors, stops, count, sx, sy, ex, ey));
}
rcp<RenderShader> makeRadialGradient(
float cx,
float cy,
float radius,
const ColorInt colors[],
const float stops[],
size_t count) override
{
return rcp<RenderShader>(
new JSRadialGradientShader(colors, stops, count, cx, cy, radius));
}
rcp<RenderShader> makeAngularGradient(
const ColorInt colors[],
const float stops[],
size_t count) override
{
return rcp<RenderShader>(new JSAngularGradientShader(colors, stops, count));
}
rcp<RenderPath> makeRenderPath(RawPath& rawPath, FillRule fillRule) override
{
rcp<RenderPath> renderPath = makeEmptyRenderPath();
renderPath->addRawPath(rawPath);
renderPath->fillRule(fillRule);
return renderPath;
}
rcp<RenderPath> makeEmptyRenderPath() override
{
return make_rcp<JSRenderPath>(
m_jsFactory.call<emscripten::val>("makeRenderPath"));
}
rcp<RenderPaint> makeRenderPaint() override
{
return make_rcp<JSRenderPaint>(
m_jsFactory.call<emscripten::val>("makeRenderPaint"));
}
rcp<RenderImage> decodeImage(Span<const uint8_t> bytes) override
{
if (bytes.empty())
{
return nullptr;
}
// Blocks until the <img> load event fires.
return make_rcp<JSRenderImage>(
emscripten::val::take_ownership(testingWindowCanvas2dDecodeImage(
bytes.data(),
static_cast<int>(bytes.size()))));
}
private:
emscripten::val m_jsFactory;
};
class JSRenderer : public rive::Renderer
{
public:
JSRenderer()
: m_jsRenderer(canvasAdvancedModule().call<emscripten::val>(
"makeRenderer",
emscripten::val::global("document")
.call<emscripten::val>(
"getElementById",
std::string("canvas"))))
{}
// renderer.js holds this renderer in _pendingCanvasRenderers until the draw
// list is flushed.
~JSRenderer() override { m_jsRenderer.call<void>("deleteLater"); }
void beginFrame(bool clear) { m_jsRenderer.call<void>("beginFrame", clear); }
void save() override { m_jsRenderer.call<void>("save"); }
void restore() override { m_jsRenderer.call<void>("restore"); }
void transform(const Mat2D& matrix) override
{
m_jsRenderer.call<void>(
"transform",
matrix.xx(),
matrix.xy(),
matrix.yx(),
matrix.yy(),
matrix.tx(),
matrix.ty());
}
void drawPath(RenderPath* path, RenderPaint* paint) override
{
auto* jsPath = rive::lite_rtti_cast_or_assert<JSRenderPath*>(path);
auto* jsPaint = rive::lite_rtti_cast_or_assert<JSRenderPaint*>(paint);
m_jsRenderer.call<void>("_drawPath", jsPath->js(), jsPaint->js());
}
void clipPath(RenderPath* path) override
{
auto* jsPath = rive::lite_rtti_cast_or_assert<JSRenderPath*>(path);
m_jsRenderer.call<void>("_clipPath", jsPath->js());
}
void drawImage(
const RenderImage* image,
ImageSampler sampler,
BlendMode blendMode,
float opacity) override
{
auto* jsImage = rive::lite_rtti_cast_or_assert<const JSRenderImage*>(image);
m_jsRenderer.call<void>(
"_drawRiveImage",
jsImage->js(),
jsEnums().blendMode(blendMode),
opacity);
}
void drawImageMesh(
const RenderImage* image,
ImageSampler sampler,
rcp<RenderBuffer> vertices_f32,
rcp<RenderBuffer> uvCoords_f32,
rcp<RenderBuffer> indices_u16,
uint32_t vertexCount,
uint32_t indexCount,
BlendMode blendMode,
float opacity) override
{
auto* vtx = rive::lite_rtti_cast_or_assert<rive::DataRenderBuffer*>(
vertices_f32.get());
auto* uv = rive::lite_rtti_cast_or_assert<rive::DataRenderBuffer*>(
uvCoords_f32.get());
auto* indices = rive::lite_rtti_cast_or_assert<rive::DataRenderBuffer*>(
indices_u16.get());
uint32_t f32Count = vertexCount * 2;
assert(vtx->sizeInBytes() == f32Count * sizeof(float));
assert(uv->sizeInBytes() == f32Count * sizeof(float));
assert(indices->sizeInBytes() == indexCount * sizeof(uint16_t));
if (f32Count == 0 || indexCount == 0)
{
return;
}
// This hands off to the C++ side of canvas_advanced so that the real
// Canvas2DRendererWrapper::drawImageMesh() runs. It owns the mesh
// bounding box computation and the atlas packing, which the harness
// would otherwise duplicate here, leaving them untested.
//
// The typed_memory_views below are over this module's heap.
// canvas_advanced can read them because a TypedArray is a plain JS
// object, whichever ArrayBuffer it wraps, and it copies into its own
// DataRenderBuffers. Nothing between here and the call allocates, so
// this module's memory cannot grow and detach them in the meantime.
auto* jsImage = rive::lite_rtti_cast_or_assert<const JSRenderImage*>(image);
canvasAdvancedModule()["Canvas2DTestUtilities"].call<void>(
"drawImageMesh",
m_jsRenderer,
jsImage->js(),
emscripten::val{emscripten::typed_memory_view(f32Count, vtx->f32s())},
emscripten::val{emscripten::typed_memory_view(f32Count, uv->f32s())},
emscripten::val{
emscripten::typed_memory_view(indexCount, indices->u16s())},
jsEnums().blendMode(blendMode),
opacity);
}
void modulateOpacity(float opacity) override
{
m_jsRenderer.call<void>("modulateOpacity", opacity);
}
private:
emscripten::val m_jsRenderer;
};
class TestingWindowCanvas2D : public TestingWindow
{
public:
TestingWindowCanvas2D()
{
testingWindowCanvas2dImportCanvasAdvanced();
int w, h;
emscripten_get_canvas_element_size("#canvas", &w, &h);
m_width = w;
m_height = h;
m_factory = std::make_unique<JSRenderFactory>();
}
rive::Factory* factory() override { return m_factory.get(); }
void resize(int width, int height) override
{
if (m_width != width || m_height != height)
{
TestingWindow::resize(width, height);
emscripten_set_canvas_element_size("#canvas", width, height);
}
}
std::unique_ptr<rive::Renderer> beginFrame(
const FrameOptions& options) override
{
m_frameName = options.name != nullptr ? options.name : "<frame>";
canvasAdvancedModule()["Canvas2DTestUtilities"].call<void>(
"setMaxCanvasAtlasSize",
options.maxCanvasAtlasSize);
if (options.doClear)
{
testingWindowCanvas2dResetCanvas(options.clearColor);
}
auto renderer = std::make_unique<JSRenderer>();
renderer->beginFrame(false);
return renderer;
}
void endFrame(std::vector<uint8_t>* pixelData) override
{
// Flush commands so that we can read pixel data from the canvas.
canvasAdvancedModule().call<void>("resolveAnimationFrame");
// The flush should have executed and released data associated with all
// pending draws.
if (int pending = canvasAdvancedModule()["Canvas2DTestUtilities"].call<int>(
"pendingAtlasReleaseCount"))
{
fprintf(
stderr,
"%s: %i delegated draw(s) were never released\n",
m_frameName.c_str(),
pending);
abort();
}
// The deferred draw list has run, so nothing on the JS side still holds
// the objects the shims above queued with deleteLater(). embind
// attaches no finalizer to raw pointer handles, so without this call
// every path, paint and renderer accumulates in canvas_advanced's heap
// for the whole run.
canvasAdvancedModule().call<void>("flushPendingDeletes");
if (!pixelData)
{
return;
}
pixelData->resize(m_width * m_height * 4);
if (!testingWindowCanvas2dGetCanvasPixels(
pixelData->data(),
m_width,
m_height))
{
printf("Canvas size mismatch, read failed\n");
pixelData->assign(pixelData->size(), 0);
return;
}
// getImageData() hands back unpremultiplied RGBA. Multiply by alpha to
// match the other backends.
for (size_t i = 0; i < pixelData->size(); i += 4)
{
uint32_t a = (*pixelData)[i + 3];
for (size_t c = 0; c < 3; ++c)
{
(*pixelData)[i + c] = static_cast<uint8_t>(
((*pixelData)[i + c] * a + 127) / 255);
}
}
}
private:
std::unique_ptr<JSRenderFactory> m_factory;
std::string m_frameName;
};
}; // namespace rive::gpu
TestingWindow* TestingWindow::MakeCanvas2D()
{
return new rive::gpu::TestingWindowCanvas2D();
}
#endif