blob: a045d97c4bfd5802c849c7a97eb70159585b4d99 [file] [edit]
/*
* Copyright 2026 Rive
*/
#pragma once
#include "rive/async/compare_exchange.hpp"
#include "rive/async/threading.hpp"
#include "rive/refcnt.hpp"
#include "rive/span.hpp"
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <functional>
#include <mutex>
#include <random>
#include <thread>
#include <vector>
// Where a lane thread chooses its own stack, as std::thread cannot; a web
// build's pthread stacks come out of its own wasm memory.
#if defined(_WIN32)
#define RIVE_LANE_NATIVE_THREADS() 1
#elif !defined(__EMSCRIPTEN__) && (defined(__APPLE__) || defined(__linux__))
#define RIVE_LANE_NATIVE_THREADS() 1
#include <pthread.h>
#else
#define RIVE_LANE_NATIVE_THREADS() 0
#endif
namespace rive
{
/// One unit of script job work. A task runs at most once, on whichever thread
/// claims it first: a pool thread, or a joiner that finds it unstarted.
class JobTask : public RefCnt<JobTask>
{
public:
enum class State : uint8_t
{
pending,
running,
done,
cancelled,
};
virtual ~JobTask() = default;
State state() const { return (State)m_state.load(std::memory_order_acquire); }
bool finished() const
{
State s = state();
return s == State::done || s == State::cancelled;
}
/// Pending to running, exactly once across every thread.
bool tryClaim()
{
uint8_t expected = (uint8_t)State::pending;
return compareExchangeStrong(
m_state,
expected,
(uint8_t)State::running,
std::memory_order_acq_rel);
}
bool cancelRequested() const
{
return m_cancelRequested.load(std::memory_order_acquire);
}
protected:
/// Runs on the claiming thread, to its end.
virtual void run() = 0;
/// A cancel reached the task while it runs; stop it promptly.
virtual void interrupt() {}
private:
friend class JobPool;
std::atomic<uint8_t> m_state{(uint8_t)State::pending};
std::atomic<bool> m_cancelRequested{false};
};
/// The order pool threads take queued tasks in. Results never depend on it;
/// the non default orders exist to prove that in tests.
enum class JobSchedule : uint8_t
{
fifo,
#ifdef WITH_RIVE_TOOLS
reverse,
shuffle,
#endif
};
#if RIVE_LANE_NATIVE_THREADS()
#ifdef _WIN32
// A HANDLE, without windows.h.
using LaneNativeThread = void*;
#else
using LaneNativeThread = pthread_t;
#endif
#endif
/// A thread that runs script lanes: the factory's, or one with kStackBytes of
/// stack.
class LaneThread
{
public:
/// As much as a main thread has on desktops and Android, so code that runs
/// on the main lane runs on every lane.
static constexpr size_t kStackBytes = 8u << 20;
LaneThread() = default;
explicit LaneThread(std::thread thread) : m_thread(std::move(thread)) {}
LaneThread(LaneThread&& other) noexcept;
LaneThread& operator=(LaneThread&&) = delete;
LaneThread(const LaneThread&) = delete;
~LaneThread();
static LaneThread start(std::function<void()> body);
void join();
void detach();
private:
std::thread m_thread;
#if RIVE_LANE_NATIVE_THREADS()
LaneNativeThread m_native{};
bool m_hasNative = false;
#endif
};
/// Script jobs' pool, apart from the decodes' WorkPool. Without threads a
/// task runs only when joined or pumped, every platform's inline fallback.
class JobPool
{
public:
using ThreadFactory = std::function<std::thread(std::function<void()>)>;
JobPool(
uint32_t threadCount,
JobSchedule schedule = JobSchedule::fifo,
uint64_t seed = 0);
~JobPool();
JobPool(const JobPool&) = delete;
JobPool& operator=(const JobPool&) = delete;
/// Consoles set this to pick cores, priorities and stack sizes.
static void setThreadFactory(ThreadFactory factory);
/// A thread from the factory, for every thread that runs script lanes.
static LaneThread makeThread(std::function<void()> body);
uint32_t threadCount() const
{
#if RIVE_POOL_THREADS()
return (uint32_t)m_threads.size();
#else
return 0;
#endif
}
void submit(rcp<JobTask> task);
/// Runs an unstarted task here to its end when runHere, else waits up
/// to timeoutSeconds for a thread to, if there are any; false when it
/// is still unfinished.
bool join(JobTask* task, double timeoutSeconds, bool runHere = true);
/// Index of the first finished task, else with runHere of the lowest
/// unstarted one, which it runs as join does; -1 when none finished.
int joinAny(Span<JobTask*> tasks, double timeoutSeconds, bool runHere = true);
/// Pending tasks are dropped; a running task is asked to stop.
void cancel(JobTask* task);
/// Runs up to max queued tasks on the caller; the zero thread pump.
uint32_t runPending(uint32_t max);
/// Runs queued tasks on the caller while within seconds, at least one;
/// nothing bounds a task once it runs.
uint32_t runPendingFor(double seconds);
bool hasQueued();
private:
void workerLoop();
rcp<JobTask> takeLocked();
uint32_t runQueued(uint32_t max, std::chrono::steady_clock::time_point until);
void execute(rcp<JobTask> task);
void notifyFinished();
// rive-cli defines TESTING for its tests only, not its library.
#ifdef WITH_RIVE_TOOLS
JobSchedule m_schedule;
std::mt19937_64 m_rng;
#endif
std::mutex m_mutex;
std::condition_variable m_workCv;
std::condition_variable m_doneCv;
std::deque<rcp<JobTask>> m_queue;
std::vector<LaneThread> m_threads;
bool m_stopping = false;
};
} // namespace rive