blob: 7aa6c310f962caccb99ef9f254275704058c181f [file] [edit]
/*
* Copyright 2022 Rive
*/
#ifndef TESTING_WINDOW_HPP
#define TESTING_WINDOW_HPP
#include "common/offscreen_render_target.hpp"
#include "rive/renderer/gpu.hpp"
#include "rive/renderer/shader_compilation_mode.hpp"
#include "rive/renderer/triangulation_controller.hpp"
#include "rive/refcnt.hpp"
#include <limits>
#include <memory>
#include <vector>
#include <string>
// Every harness that compares rendered pixels against a baseline (gms,
// goldens) must render deterministically, so none of them impose a frame-time
// limit on triangulation: a path meant to exercise triangulation has to do so
// on every run and machine. The guards still apply -- GMs can tune those per
// test in updateFrameOptions().
//
// Every frame a harness opens has to use these, not just the ones that look
// like they matter: goldens' immediate and deferred paths only produce
// identical pixels if both do.
//
// Interactive tools (the player) deliberately don't use this; they want
// production budgeting behavior.
constexpr rive::gpu::TriangulationThresholds
DeterministicTriangulationThresholds = {
.frameBudgetMs = std::numeric_limits<float>::infinity(),
};
namespace rive
{
class Renderer;
class Factory;
namespace gpu
{
class RenderContext;
class RenderContextGLImpl;
class RenderTarget;
class Texture;
class TextureRenderTarget;
} // namespace gpu
}; // namespace rive
// Wraps a factory for rive::Renderer and a singleton target for it to render
// into (GL window, HTML canvas, software buffer, etc.):
//
// TestingWindow::Init(type);
// renderer = TestingWindow::Get()->reset(width, height);
// ...
//
class TestingWindow
{
public:
enum class Backend
{
gl,
d3d,
d3d12,
metal,
vk,
// Vulkan on Metal, aka MoltenVK.
// (defaults to /usr/local/share/vulkan/icd.d/MoltenVK_icd.json if
// VK_ICD_FILENAMES is not set.)
moltenvk,
// Swiftshader, Google's CPU implementation of Vulkan.
// (defaults to ./vk_swiftshader_icd.json if VK_ICD_FILENAMES is not
// set.)
swiftshader,
angle,
dawn,
wgpu,
external,
coregraphics,
skia,
canvas2d,
svg,
null,
invalid,
};
enum class Target
{
host,
android,
ios,
iossim,
unreal,
unreal_android,
webbrowser,
webserver,
webbrowserandroid,
webserverandroid,
console,
};
enum class ANGLERenderer
{
metal,
d3d11,
vk,
gles,
gl,
};
struct BackendParams
{
bool atomic = false;
bool core = false;
bool srgb = false;
bool clockwise = false;
bool disableValidationLayers = false;
bool disableDebugCallbacks = false;
bool wantVulkanSynchronizationValidation = false;
// 0 => non-depthStencil mode (if supported; else 4x MSAA)
// 1 => depthStencil w/o MSAA
// >1 => depthStencil with MSAA
uint32_t msaaSampleCount = 0;
rive::gpu::ShaderCompilationMode shaderCompilationMode =
rive::gpu::ShaderCompilationMode::standard;
ANGLERenderer angleRenderer =
#ifdef __APPLE__
ANGLERenderer::metal;
#elif defined(_WIN32)
ANGLERenderer::d3d11;
#else
ANGLERenderer::vk;
#endif
std::string gpuNameFilter;
// How a window presents: fifo waits for vblank, relaxed tears when
// late, mailbox and immediate run uncapped. Only the Artemis window
// reads this today, and it falls back to fifo when the surface lacks
// the mode.
enum class PresentMode
{
fifo,
fifoRelaxed,
mailbox,
immediate,
};
PresentMode presentMode = PresentMode::fifo;
// The display refresh a window asks for where the platform lets it
// choose; others keep their default.
enum class Refresh
{
fixed60,
fixed120,
variable,
};
Refresh refresh = Refresh::fixed60;
};
enum class Visibility
{
headless,
window,
fullscreen,
};
enum class InputEvent
{
KeyPress,
MouseDown,
MouseUp,
MouseMove,
GamepadConnected,
GamepadDisconnected,
GamepadChange,
};
// Aligns with GLFW's mouse button enum
enum MouseButton : int
{
Left,
Right,
Middle,
// can support more buttons
};
struct InputEventData
{
InputEventData() : InputEventData(InputEvent::KeyPress, '\0') {}
InputEventData(InputEvent type, float posX, float posY) :
eventType(type)
{
metadata = {.posX = posX, .posY = posY};
}
InputEventData(InputEvent type, char c) : eventType(type)
{
metadata = {.key = c};
}
// A gamepad arriving, with its standard layout button and axis counts.
InputEventData(InputEvent type,
uint32_t deviceId,
uint8_t buttonCount,
uint8_t axisCount) :
eventType(type)
{
metadata.pad = {.deviceId = deviceId,
.buttonCount = buttonCount,
.axisCount = axisCount};
}
// One standard layout button or axis changing value.
InputEventData(InputEvent type,
uint32_t deviceId,
bool isAxis,
uint8_t index,
float value) :
eventType(type)
{
metadata.pad = {.deviceId = deviceId,
.isAxis = isAxis,
.index = index,
.value = value};
}
InputEvent eventType;
union
{
struct
{
// Move & down/up mouse press events all have coords
float posX;
float posY;
};
char key;
struct
{
uint32_t deviceId;
bool isAxis;
uint8_t index;
uint8_t buttonCount;
uint8_t axisCount;
float value;
} pad;
} metadata;
};
static const char* BackendName(Backend);
static Backend TryParseBackend(const char* name, BackendParams*);
static Backend ParseBackend(const char* name, BackendParams*);
static TestingWindow* Init(Backend,
const BackendParams&,
Visibility,
void* platformWindow = nullptr);
static TestingWindow* Get();
static void Set(TestingWindow* inWindow);
static void Destroy();
static Backend backend() { return s_Backend; }
static Target target() { return s_Target; }
static void SetTarget(Target target) { s_Target = target; }
static bool isUnreal()
{
return s_Target == Target::unreal || s_Target == Target::unreal_android;
}
uint32_t width() const { return m_width; }
uint32_t height() const { return m_height; }
virtual rive::Factory* factory() = 0;
virtual void resize(int width, int height)
{
m_width = width;
m_height = height;
}
// This is called by the gm testing after each GM runs
virtual void onceAfterGM() {}
struct FrameOptions
{
const char* name;
uint32_t clearColor;
bool doClear = true;
bool forceMSAA = false;
bool disableRasterOrdering = false;
bool wireframe = false;
bool fillsDisabled = false;
bool strokesDisabled = false;
bool clockwiseFillOverride = false;
rive::gpu::TriangulationThresholds triangulationThresholds;
#ifdef WITH_RIVE_TOOLS
rive::gpu::SynthesizedFailureType synthesizedFailureType =
rive::gpu::SynthesizedFailureType::none;
rive::gpu::ShaderCompilationMode shaderCompilationMode =
rive::gpu::ShaderCompilationMode::standard;
#endif
};
virtual std::unique_ptr<rive::Renderer> beginFrame(const FrameOptions&) = 0;
virtual void endFrame(std::vector<uint8_t>* pixelData = nullptr) = 0;
// The descriptor fields that pick a frame's interlock mode.
struct FrameMode
{
uint32_t msaaSampleCount = 0;
bool disableRasterOrdering = false;
bool clockwiseFillOverride = false;
};
// Windows that add backend settings resolve them here, so a replayed
// canvas opens in the same mode as the screen.
virtual FrameMode frameMode(const FrameOptions& options) const
{
return {options.forceMSAA ? 4u : 0u,
options.disableRasterOrdering,
options.clockwiseFillOverride};
}
// Override to open a replayed canvas differently from the screen.
virtual FrameMode canvasFrameMode(const FrameOptions& options) const
{
return frameMode(options);
}
// For testing directly on RenderContext.
virtual rive::gpu::RenderContext* renderContext() const { return nullptr; }
virtual rive::gpu::RenderContextGLImpl* renderContextGLImpl() const
{
return nullptr;
}
virtual rive::gpu::RenderTarget* renderTarget() const { return nullptr; }
// Creates a new render target, for testing offscreen rendering.
// If riveRenderable is false, the texture will lack features required by
// Rive for rendering directly to it (e.g., input attachment, UAV, etc.),
// which will exercise indirect rendering fallbacks.
virtual rive::rcp<rive_tests::OffscreenRenderTarget>
makeOffscreenRenderTarget(uint32_t width,
uint32_t height,
bool riveRenderable) const
{
return nullptr;
}
// For testing render pass breaks. Caller must call
// renderContext()->beginFrame() again.
virtual void flushPLSContext(
rive::gpu::RenderTarget* offscreenRenderTarget = nullptr)
{}
virtual bool consumeInputEvent(InputEventData& eventData) { return false; }
virtual InputEventData waitForInputEvent()
{
fprintf(stderr, "TestingWindow::waitForInputEvent not implemented.");
abort();
}
virtual bool shouldQuit() const { return false; }
virtual void hotloadShaders() {}
#if defined(__APPLE__)
// Returns the Metal command queue as void* to avoid <Metal/Metal.h> in
// this cross-platform header (same pattern as externalCommandBuffer).
virtual void* metalQueue() const { return nullptr; }
#endif
#ifdef RIVE_VULKAN
// Vulkan accessors for ORE VK backend initialization in GMs.
virtual void* vulkanGraphicsQueue() const { return nullptr; }
virtual uint32_t vulkanGraphicsQueueFamilyIndex() const { return 0; }
virtual void* vulkanGetInstanceProcAddr() const { return nullptr; }
#endif
// Returns the host's currently-recording command buffer (as
// void*) Returns nullptr on backends that don't use command buffers / lists
virtual void* getCurrentCommandBuffer() const { return nullptr; }
// Host-managed ore::Context, or nullptr. Player wires into the file's
// ScriptingContext for GPUCanvas content.
virtual void* getOreContext() const { return nullptr; }
// Per-frame ORE driving; no-op on non-ORE harnesses.
virtual void beginOreFrame() {}
virtual void endOreFrame() {}
// Whether scripts may draw into renderTarget() under Rive's content, and
// its format, answered without touching it so the recording thread can
// declare it.
struct OreTarget
{
bool exposed = false;
bool bgra = false;
};
virtual OreTarget oreTarget() const { return {}; }
// Whether beginFrame through endFrame may run off the thread that made the
// window. A current GL context or GLFW event polling in endFrame says no.
virtual bool supportsRenderThread() const { return false; }
virtual ~TestingWindow() {}
static TestingWindow* MakeEGL(Backend,
const BackendParams&,
void* platformWindow);
#if defined(__APPLE__) && !defined(RIVE_UNREAL)
static TestingWindow* MakeMetalTexture(const BackendParams&);
#endif
#if defined(RIVE_IOS) || defined(RIVE_IOS_SIMULATOR)
static TestingWindow* MakeMetalLayer(const BackendParams&,
void* platformWindow);
#endif
static TestingWindow* MakeCoreGraphics();
static TestingWindow* MakeFiddleContext(Backend,
const BackendParams&,
Visibility,
void* platformWindow);
static TestingWindow* MakeVulkanTexture(const BackendParams&);
static TestingWindow* MakeAndroidVulkan(const BackendParams&,
void* platformWindow);
// Hook for platforms without rive_vk_bootstrap; default returns nullptr.
// Platform packages override in their own TU + define
// RIVE_PLATFORM_TESTING_WINDOW.
static TestingWindow* MakePlatformVulkan(const BackendParams&);
static TestingWindow* MakeWGPU(const BackendParams&);
static TestingWindow* MakeSVG();
static TestingWindow* MakeSkia();
static TestingWindow* MakeCanvas2D();
static TestingWindow* MakeNULL();
protected:
uint32_t m_width = 0;
uint32_t m_height = 0;
static Backend s_Backend;
static Target s_Target;
};
#endif