blob: 161b0bc524aba9c06e6f88c25c37bb31d601f7c8 [file]
#ifndef _RIVE_MODULE_TIER_LADDER_HPP_
#define _RIVE_MODULE_TIER_LADDER_HPP_
#ifdef WITH_RIVE_SCRIPTING_WASM
// An editor/tools feature: the ladder spawns wamrc subprocesses. Runtime
// builds stub it out and stay on interp (prelinked artifacts load by
// registry instead); windows tools builds stay stubbed pending a
// CreateProcess port.
#if defined(WITH_RIVE_TOOLS) && !defined(_WIN32)
#define RIVE_WASM_TIER_LADDER 1
#endif
#include "rive/span.hpp"
#include <cstdint>
#include <functional>
#include <string>
#ifdef RIVE_WASM_TIER_LADDER
#include <condition_variable>
#include <deque>
#include <mutex>
#include <sys/types.h>
#include <thread>
#include <unordered_map>
#include <vector>
#endif
namespace rive
{
// Compiled-artifact species, in ascending speed. Interp is the implicit
// tier 0 every module already runs on; the ladder only produces artifacts.
enum class TierSpecies : uint8_t
{
o0 = 0, // wamrc -O0: ~2x interp, sub-second for typical modules
o3 = 1, // wamrc top opt level with sw bounds, runs on every build
// No inline bounds checks; needs the guard-page trap handler, so only
// RIVE_WASM_HW_BOUNDS builds produce or load these.
hw = 2,
};
#ifdef RIVE_WASM_TIER_LADDER
// Drives wamrc subprocesses that turn wasm modules into AOT artifacts and
// reports arrivals for frame-boundary hot swap. File-in/file-out, no IPC:
// compile crashes and LLVM's RSS stay in the child. Process-wide like the
// shared module cache; safe to call from any thread.
class ModuleTierLadder
{
public:
static ModuleTierLadder& instance();
// wamrc discovery: explicit path wins, RIVE_WAMRC env second. The
// ladder is inert (schedule() == no-op) until both a compiler and a
// cache directory are configured.
void configure(const std::string& wamrcPath, const std::string& cacheDir);
bool enabled();
struct Artifact
{
uint64_t moduleKey = 0;
TierSpecies species = TierSpecies::o0;
std::string path;
};
using ArrivalCallback = std::function<void(const Artifact&)>;
// One process-wide sink; the editor fans out. Called from ladder worker
// threads — receivers defer the swap to a frame boundary.
void onArrival(ArrivalCallback callback);
// Schedule compiles for a module. laneId groups requests for the same
// logical script: a newer schedule on the lane kills superseded
// in-flight compiles (newest-hash-wins). Small modules go straight to
// -O3; larger ones run -O0 and -O3 in parallel. Cache hits report
// arrival immediately without spawning.
void schedule(const std::string& laneId,
uint64_t moduleKey,
Span<const uint8_t> moduleBytes);
// Write the module's wamrc input before wasm_runtime_load touches the
// buffer: the fast-interp loader rewrites it in place, so bytes staged
// at schedule time are no longer valid wasm.
void stagePristine(uint64_t moduleKey, Span<const uint8_t> moduleBytes);
// Ready artifact path for a module at the given species, empty if none.
std::string artifactPath(uint64_t moduleKey, TierSpecies species);
// Test hooks: block until the lane has no queued or running compiles.
void drain();
// Straight -O3 cutoff; modules at or under skip the -O0 rung entirely.
static constexpr size_t kStraightToO3Bytes = 50 * 1024;
private:
ModuleTierLadder() = default;
~ModuleTierLadder();
struct Job
{
std::string laneId;
uint64_t moduleKey = 0;
TierSpecies species = TierSpecies::o0;
std::string wasmPath;
uint64_t generation = 0;
pid_t pid = -1;
bool cancelled = false;
};
void ensureWorkers();
void workerLoop();
bool runWamrc(Job& job);
std::string artifactName(uint64_t moduleKey, TierSpecies species);
std::string keyedCacheDir();
const std::string& wamrcVersion();
std::mutex m_mutex;
std::condition_variable m_workAvailable;
std::condition_variable m_idle;
std::string m_wamrcPath;
std::string m_cacheDir;
std::string m_wamrcVersion;
bool m_versionProbed = false;
ArrivalCallback m_arrival;
std::deque<Job> m_queue;
std::vector<Job*> m_running;
// Latest schedule generation per lane; older jobs are stale and are
// killed (running) or skipped (queued).
std::unordered_map<std::string, uint64_t> m_laneGeneration;
uint64_t m_nextGeneration = 1;
std::vector<std::thread> m_workers;
bool m_terminating = false;
// Two concurrent compiles saturate the edit loop without starving the
// machine; the -O3 tail rides behind the -O0 rung it bridges.
static constexpr unsigned kWorkerCount = 2;
};
#else // RIVE_WASM_TIER_LADDER
// Same surface, all inline: the ladder reports disabled, so the tier
// paths behind enabled() checks compile away.
class ModuleTierLadder
{
public:
static ModuleTierLadder& instance()
{
static ModuleTierLadder ladder;
return ladder;
}
void configure(const std::string&, const std::string&) {}
bool enabled() { return false; }
struct Artifact
{
uint64_t moduleKey = 0;
TierSpecies species = TierSpecies::o0;
std::string path;
};
using ArrivalCallback = std::function<void(const Artifact&)>;
void onArrival(ArrivalCallback) {}
void schedule(const std::string&, uint64_t, Span<const uint8_t>) {}
void stagePristine(uint64_t, Span<const uint8_t>) {}
std::string artifactPath(uint64_t, TierSpecies) { return std::string(); }
void drain() {}
static constexpr size_t kStraightToO3Bytes = 50 * 1024;
private:
// Singleton in both configurations, so code cannot compile against one
// and break against the other.
ModuleTierLadder() = default;
~ModuleTierLadder() = default;
ModuleTierLadder(const ModuleTierLadder&) = delete;
ModuleTierLadder& operator=(const ModuleTierLadder&) = delete;
};
#endif // RIVE_WASM_TIER_LADDER
} // namespace rive
#endif // WITH_RIVE_SCRIPTING_WASM
#endif