blob: 8e7d7955cc1cb0d5b096ef3c20c43be9cc20ca41 [file]
#include "rive/shapes/paint/artboard_paint_source.hpp"
#include "rive/animation/linear_animation_instance.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/artboard.hpp"
#include "rive/data_bind/data_context.hpp"
#include "rive/renderer.hpp"
#ifdef RIVE_CANVAS
#include "rive/offscreen_raster.hpp"
#include "rive/renderer/cmd/deferred_canvas_host.hpp"
#include "rive/renderer/render_canvas.hpp"
#endif
#include <algorithm>
#include <cmath>
#include <limits>
using namespace rive;
namespace
{
// Instancing a source instances the paint sources inside it, so a reference
// cycle recurses without end. The importer cuts every cycle it can see, but
// the editor splices artboards in after import, and a cycle closed by the
// spliced artboard is never re-checked -- so this is the backstop.
constexpr int kMaxNesting = 16;
thread_local int s_nesting = 0;
#ifdef RIVE_CANVAS
// Texels a raster needs along one axis: [span] device pixels covering
// [extent] artboard units. The per-unit scale is quantized up to sixteenths,
// as planRasterScale does, so a zoom or a scale tween does not re-raster at a
// new size on every frame.
//
// Stays a float: an extreme zoom can ask for more texels than a uint32_t
// holds, and converting that is undefined (x86 wraps it to an arbitrary,
// often tiny size that slips under the cap). The caller caps first, then
// converts. An overflow to infinity comes back as the largest float, so the
// cap can still shrink it.
float texelsFor(float span, float extent)
{
if (!(span > 0.0f) || !std::isfinite(span))
{
return 1.0f;
}
constexpr float kScaleQuantum = 16.0f;
const float perUnit = std::ceil(span / extent * kScaleQuantum) /
kScaleQuantum;
return std::min(
std::max(1.0f, std::ceil(extent * perUnit)),
std::numeric_limits<float>::max());
}
#endif
} // namespace
ArtboardPaintSource::ArtboardPaintSource(
std::unique_ptr<ArtboardInstance> artboard)
: m_artboard(std::move(artboard))
{}
// Out of line so the scene instances and the rcp<gpu::RenderCanvas> are only
// destroyed where their types are complete.
ArtboardPaintSource::~ArtboardPaintSource() = default;
std::unique_ptr<ArtboardPaintSource> ArtboardPaintSource::make(
const Artboard* source,
Factory* factory,
ArtboardHost* host)
{
if (source == nullptr || s_nesting >= kMaxNesting)
{
return nullptr;
}
s_nesting++;
std::unique_ptr<ArtboardInstance>
instance = source->instance<ArtboardInstance>(factory);
s_nesting--;
if (instance == nullptr)
{
return nullptr;
}
if (host != nullptr)
{
instance->host(host);
}
std::unique_ptr<ArtboardPaintSource> paintSource(
new ArtboardPaintSource(std::move(instance)));
ArtboardInstance* artboard = paintSource->m_artboard.get();
// The order ArtboardInstance::defaultScene picks in, kept typed so advance
// can apply NestedArtboard's per-kind rules.
paintSource->m_stateMachine = artboard->defaultStateMachine();
if (paintSource->m_stateMachine == nullptr)
{
paintSource->m_stateMachine = artboard->stateMachineAt(0);
}
if (paintSource->m_stateMachine == nullptr)
{
paintSource->m_animation = artboard->animationAt(0);
}
// Settle the first frame so the first raster shows the initial state
// rather than the setup pose.
paintSource->advance(
0.0f,
AdvanceFlags::AdvanceNested | AdvanceFlags::Animate |
AdvanceFlags::NewFrame);
paintSource->update();
return paintSource;
}
bool ArtboardPaintSource::advance(float elapsedSeconds, AdvanceFlags flags)
{
bool keepGoing = false;
const bool advanceNested = (flags & AdvanceFlags::AdvanceNested) ==
AdvanceFlags::AdvanceNested;
const bool newFrame = (flags & AdvanceFlags::NewFrame) ==
AdvanceFlags::NewFrame;
if (advanceNested)
{
if (m_stateMachine != nullptr)
{
// As NestedArtboard does: off a new frame, only advance once the
// state actually changed, so intermediate values that settle back
// where they were do not dirty the artboard.
if (newFrame || m_stateMachine->tryChangeState())
{
if (m_stateMachine->advance(elapsedSeconds, newFrame))
{
keepGoing = true;
}
}
}
else if (m_animation != nullptr && newFrame)
{
if (m_animation->advance(elapsedSeconds))
{
keepGoing = true;
}
m_animation->apply();
}
}
if (m_artboard->advanceInternal(
elapsedSeconds,
flags & ~AdvanceFlags::IsRoot))
{
keepGoing = true;
}
return keepGoing;
}
bool ArtboardPaintSource::needsUpdate() const
{
return m_artboard->hasDirt(ComponentDirt::Components);
}
void ArtboardPaintSource::update() { m_artboard->updatePass(false); }
AABB ArtboardPaintSource::bounds() const { return m_artboard->bounds(); }
void ArtboardPaintSource::dataContext(rcp<DataContext> dataContext)
{
if (dataContext == nullptr)
{
clearDataContext();
return;
}
// The non-stateful, no-globals path of NestedArtboard::internalDataContext:
// the instance shares its host's context rather than getting its own.
m_artboard->internalDataContext(dataContext);
if (m_stateMachine != nullptr)
{
m_stateMachine->dataContext(dataContext);
}
}
void ArtboardPaintSource::relinkDataContext(
rcp<ViewModelInstance> viewModelInstance)
{
// Mirrors NestedArtboard::relinkDataContext. With a passed-through context
// the instance's context is the host's own, so its main instance already
// matches and only the binds below need relinking.
if (auto dataContext = m_artboard->dataContext())
{
if (dataContext->mainViewModelInstance() != viewModelInstance)
{
dataContext->viewModelInstance(viewModelInstance);
}
}
m_artboard->relinkDataContext();
}
void ArtboardPaintSource::clearDataContext()
{
m_artboard->clearDataContext();
if (m_stateMachine != nullptr)
{
m_stateMachine->clearDataContext();
}
}
void ArtboardPaintSource::unbind() { m_artboard->unbind(); }
void ArtboardPaintSource::updateDataBinds() { m_artboard->updateDataBinds(); }
#ifdef RIVE_CANVAS
bool ArtboardPaintSource::rasterize(cmd::DeferredCanvasHost* host)
{
Renderer* renderer = host->beginCanvasContent(m_canvas.get(), 0);
if (renderer == nullptr)
{
// The host could not open an offscreen frame, so the canvas holds
// whatever it held before (uninitialized on a first attempt). Drop it
// and stay dirty: the next draw retries instead of sampling a hole.
m_canvas.reset();
m_widthPx = 0;
m_heightPx = 0;
m_dirty = true;
return false;
}
// Stretch the artboard's box over the whole canvas rather than drawing at
// a uniform scale: the paint maps the image's 0..1 space onto the shape,
// so the texture's edges have to be exactly the box's edges, or tiling
// would repeat the slack that rounding (and a held, larger allocation)
// leaves at the far side.
const AABB box = bounds();
renderer->save();
renderer->transform(Mat2D::fromScale(
(float)m_widthPx / box.width(),
(float)m_heightPx / box.height()));
renderer->translate(-box.left(), -box.top());
// drawContent, not draw(): draw() is the root entry point, which advances
// the frame id and can divert to the watermark.
m_artboard->drawContent(renderer);
renderer->restore();
host->endCanvasContent(m_canvas.get());
m_dirty = false;
#ifdef TESTING
m_rasterCount++;
#endif
return true;
}
#endif
bool ArtboardPaintSource::applyTo(
Renderer* renderer,
cmd::DeferredCanvasHost* host,
RenderPaint* paint,
ImageSampler sampler,
const Mat2D& imageTransform,
ModulatedDrawTarget* target)
{
target->renderer = renderer;
#ifdef RIVE_CANVAS
if (host == nullptr)
{
return false;
}
const AABB box = bounds();
if (!(box.width() > 0.0f) || !(box.height() > 0.0f))
{
return false;
}
// How many device pixels the image's unit square spans along each of its
// axes: the renderer's transform (zoom, density, mount) times the paint's
// own fit, scale and rotation. A renderer that reports no transform counts
// as identity, which rasterizes at one texel per local unit.
Mat2D ctm;
const bool haveCtm = renderer->currentTransform(&ctm);
const Mat2D toDevice = haveCtm ? ctm * imageTransform : imageTransform;
float wantW = texelsFor(
Vec2D(toDevice.xx(), toDevice.xy()).length(),
box.width());
float wantH = texelsFor(
Vec2D(toDevice.yx(), toDevice.yy()).length(),
box.height());
// Cap by shrinking both axes together, so a capped raster keeps the
// proportions it would have had. Done in float, before converting, since
// the request can exceed what a uint32_t holds.
const float maxDim = (float)offscreen::kMaxDim;
const float largest = std::max(wantW, wantH);
if (largest > maxDim)
{
const float shrink = maxDim / largest;
wantW = std::min(wantW * shrink, maxDim);
wantH = std::min(wantH * shrink, maxDim);
}
const uint32_t needW = std::max<uint32_t>(1, (uint32_t)wantW);
const uint32_t needH = std::max<uint32_t>(1, (uint32_t)wantH);
// Growth reallocates at once; shrinking waits out a streak, so a source
// animating through a size does not mint a texture per frame. A held
// allocation larger than needed is still exact, because the content is
// stretched over whatever the canvas is.
const offscreen::RasterResize resize = offscreen::decideRasterResize(
needW,
needH,
m_canvas != nullptr ? m_widthPx : 0,
m_canvas != nullptr ? m_heightPx : 0,
&m_shrinkStreak);
if (resize.reallocate || m_canvas == nullptr)
{
m_canvas = host->makeContentCanvas(resize.widthPx, resize.heightPx);
m_widthPx = m_canvas != nullptr ? resize.widthPx : 0;
m_heightPx = m_canvas != nullptr ? resize.heightPx : 0;
// A new texture holds nothing.
m_dirty = true;
}
if (m_canvas == nullptr)
{
return false;
}
if ((m_dirty || m_artboard->didChange()) && !rasterize(host))
{
return false;
}
// Not necessarily the canvas's own image: some backends cannot sample
// their canvas textures directly and stand in a companion for it.
rcp<RenderImage> image = host->contentCanvasImage(m_canvas.get());
if (image == nullptr)
{
return false;
}
// An immediate host interrupted the renderer to rasterize, and that
// renderer can no longer sample the canvas; draw through a clean one, as
// offscreen::beginComposite does for a cached artboard.
float opacity = 1.0f;
if (haveCtm && renderer->currentModulatedOpacity(&opacity))
{
if (Renderer* fresh = host->compositeRenderer())
{
target->renderer = fresh;
target->transform = ctm;
target->opacity = opacity;
}
}
paint->modulatedImage(image.get(), sampler, imageTransform);
return true;
#else
// Offscreen canvases are compiled out; the paint draws without an image.
(void)host;
(void)paint;
(void)sampler;
(void)imageTransform;
return false;
#endif
}
#ifdef TESTING
uint32_t ArtboardPaintSource::rasterWidth() const
{
#ifdef RIVE_CANVAS
return m_canvas != nullptr ? m_widthPx : 0;
#else
return 0;
#endif
}
uint32_t ArtboardPaintSource::rasterHeight() const
{
#ifdef RIVE_CANVAS
return m_canvas != nullptr ? m_heightPx : 0;
#else
return 0;
#endif
}
#endif