blob: 1d08166e78fe554a8c02588676e57238b5500d0d [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, (), {
// The import itself is async, and then we need to call the "default"
// function to get the module, which is also async.
// 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). The fill has to
// happen here rather than through the renderer: a drawPath composites over the
// existing pixels, which cannot produce a transparent clear color.
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 exactly 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() flips vertically on the way out, since every
// other backend fills this buffer bottom-up (the GL convention).
// getImageData() hands back rows top-down, so reverse them here and
// let the two cancel.
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 only fills in the result on its load event.
//
// Note this goes through Module.decodeImage rather than the more obvious
// renderFactory.makeRenderImage(). The latter's onDecode callback closes over
// renderer.js's loadContext, which is only non-null inside Module.load(), and
// this harness draws images directly without ever loading a .riv file, so that
// path would dereference null on the first decode.
EM_ASYNC_JS(emscripten::EM_VAL,
testingWindowCanvas2dDecodeImage,
(const uint8_t* bytes, int size),
{
// Copy out of our heap up front just to be extra safe
// (renderer.js's `decode` function also makes a Blob copy).
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 rather than at static-init time, since the module isn't
// 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, not plain numbers, and
// renderer.js compares them by identity. This class maps the C++ int-based
// enums to the objects stored in canvas_advanced's heap (which is different
// from this wasm module's heap!).
//
// embind stores the reverse map on the enum's JS constructor as
// `.values[rawValue]` (see _embind_register_enum_value in libembind.js), which
// lets us build the table without naming every enumerator here.
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?
// Index the table by raw value. Some of these enums are sparse, so the
// gaps stay undefined and are caught by the assert in operator().
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);
// We're storing the enum mapping in a vector rather than a sparse
// data structure, like a map. If we ever try to register an enum
// with really high values, it might indicate that this enum is
// meant to be used as a bit-mask, and the vector will become huge.
// If that happens, this assert will trigger and we can switch to
// something more appropriate.
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;
};
// The names here are the ones canvas_advanced registers in
// EMSCRIPTEN_BINDINGS(RiveWASM_C2D); they are not the C++ type names.
struct JSEnums
{
JSEnum blendMode{"BlendMode"};
JSEnum fillRule{"FillRule"};
JSEnum paintStyle{"RenderPaintStyle"};
JSEnum strokeCap{"StrokeCap"};
JSEnum strokeJoin{"StrokeJoin"};
};
// Built on first use, which must be after the import wait loop in
// TestingWindowCanvas2D's constructor.
static const JSEnums& jsEnums()
{
static JSEnums enums;
return enums;
}
// The classes below let this module drive the canvas2d renderer that lives in
// canvas_advanced.mjs.
//
// This harness and canvas_advanced are separately linked Emscripten modules:
// two WebAssembly.Memory instances, two C++ runtimes, two embind type
// registries. A pointer is only 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 only one JavaScript
// realm; that includes TypedArrays, which are ordinary JS objects that happen
// to view a particular module's heap.
//
// So each class holds the emscripten::val of its canvas_advanced counterpart
// and forwards to it, either directly for simple cases (e.g.
// m_jsPath.call<void>("moveTo", x, y)), or through Canvas2DTestUtilities, where
// that wrapper has logic worth exercising rather than duplicating.
//
// Ownership runs opposite to bindings_c2d.cpp: there the factory adopts the C++
// object out of the JS handle, whereas here the handle stays the sole owner,
// and embind never reclaims raw pointer handles by itself. So each destructor
// releases explicitly, via deleteLater() rather than delete() because
// renderer.js captures paths and paints in deferred draw-list closures that
// must outlive us until endFrame() flushes them.
//
// Every renderer object we see originates from JSRenderFactory, so a failed
// downcast is a bug rather than a legitimate "some other subclass" case --
// hence lite_rtti_cast_or_assert<> rather than the silent LITE_RTTI_CAST_OR_*
// macros.
// Gradients have no JS-side object to wrap, so there is no handle for
// makeLinearGradient() to return. Instead we hold the parameters and replay
// them onto the paint when JSRenderPaint::shader() is called, mirroring
// GradientShader 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 wants the radius expressed as a second point rather than
// a scalar. Matches RadialGradientShader::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 JSRenderPath : public LITE_RTTI_OVERRIDE(RenderPath, JSRenderPath)
{
public:
JSRenderPath(emscripten::val&& jsPath) :
m_jsPath(std::forward<emscripten::val>(jsPath))
{}
// Queued rather than deleted outright: renderer.js's _drawPath/_clipPath
// capture the path in deferred draw-list closures, so it has to outlive us
// until TestingWindowCanvas2D::endFrame() flushes them. See the comment on
// flushPendingDeletes() there.
~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 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 feather(float value) override
{
// Not currently implemented (yet)
}
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 invalidateStroke() override {}
emscripten::val& js() { return m_jsPaint; }
const emscripten::val& js() const { return m_jsPaint; }
private:
emscripten::val m_jsPaint;
};
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,
// which sets the dimensions on the wrapper in canvas_advanced's heap.
// Ours is a separate rive::RenderImage with its own m_Width/m_Height,
// and rive core reads those, 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 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. Must be a DataRenderBuffer for the LITE_RTTI casts
// there to succeed.
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<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"))))
{}
// Queued, not deleted: renderer.js holds us 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;
}
// Unlike the rest of the shims, this one hands off to the C++ side of
// canvas_advanced rather than to renderer.js, so that the real
// RendererWrapper::drawImageMesh() runs -- it owns the mesh bounding
// box computation and the atlas packing, and we'd otherwise have to
// duplicate them here (and leave them untested).
//
// The typed_memory_views below are over our heap. A TypedArray is a
// plain JS object, so canvas_advanced can read it even though it wraps
// a different ArrayBuffer; it copies into its own DataRenderBuffers.
// Nothing between here and the call allocates locally, so our memory
// can't 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
{
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");
// Now that the deferred draw list has run, nothing on the JS side is
// still holding the objects our shims queued via deleteLater(). Embind
// never reclaims these on its own -- class handles created from JS own
// their C++ instance outright, and no finalizer is attached to raw
// pointer handles -- so without this every path, paint and renderer
// would accumulate 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;
};
}; // namespace rive::gpu
TestingWindow* TestingWindow::MakeCanvas2D()
{
return new rive::gpu::TestingWindowCanvas2D();
}
#endif