110 lines
4.2 KiB
C++
110 lines
4.2 KiB
C++
#include "Gallery_Frame_Clock.hpp"
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
#include <stdexcept>
|
|
#include <thread>
|
|
#include <utility>
|
|
|
|
namespace aethera::web::detail {
|
|
struct Gallery_Frame_Clock::Private {
|
|
std::chrono::nanoseconds interval{};
|
|
Tick_Handler tick_handler{};
|
|
Failure_Handler failure_handler{};
|
|
/* condition_variable_any 的 stop_token 等待和 jthread 替换必须共享生命周期
|
|
* 临界区;它不是业务状态锁,双缓冲不能替代等待协议。 */
|
|
std::mutex lifecycle_mutex{};
|
|
std::condition_variable_any wake{};
|
|
std::jthread thread{};
|
|
std::atomic_bool running{};
|
|
std::atomic_uint64_t generation{}; /* 区分 stop 期间接管的新时钟。 */
|
|
|
|
Private(double frame_rate, Tick_Handler value_tick_handler,
|
|
Failure_Handler value_failure_handler)
|
|
: tick_handler(std::move(value_tick_handler)),
|
|
failure_handler(std::move(value_failure_handler)) {
|
|
if (!std::isfinite(frame_rate) || frame_rate <= 0.0)
|
|
throw std::invalid_argument("gallery frame rate must be positive");
|
|
interval = std::chrono::nanoseconds{
|
|
static_cast<std::int64_t>(std::llround(1'000'000'000.0 / frame_rate))};
|
|
if (interval.count() <= 0)
|
|
throw std::invalid_argument("gallery frame rate exceeds clock resolution");
|
|
if (!tick_handler)
|
|
throw std::invalid_argument("gallery frame clock requires a tick handler");
|
|
}
|
|
|
|
void report(std::uint64_t active_generation,
|
|
std::exception_ptr failure) noexcept {
|
|
if (generation.load(std::memory_order_acquire) == active_generation)
|
|
running.store(false, std::memory_order_release);
|
|
if (!failure_handler) return;
|
|
try { failure_handler(std::move(failure)); }
|
|
catch (...) {}
|
|
}
|
|
|
|
void run(std::stop_token stop, std::uint64_t active_generation) noexcept {
|
|
const auto origin = std::chrono::steady_clock::now();
|
|
std::uint64_t sequence{};
|
|
auto deadline = origin + interval;
|
|
while (!stop.stop_requested()) {
|
|
std::unique_lock lock(lifecycle_mutex);
|
|
wake.wait_until(lock, stop, deadline, [] { return false; });
|
|
lock.unlock();
|
|
if (stop.stop_requested()) break;
|
|
try {
|
|
++sequence;
|
|
tick_handler(Plot_Render_Tick{
|
|
std::chrono::steady_clock::now(),
|
|
sequence,
|
|
std::chrono::duration<double, std::milli>(deadline - origin).count()});
|
|
deadline += interval;
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (deadline <= now)
|
|
deadline += interval * ((now - deadline) / interval + 1);
|
|
}
|
|
catch (...) {
|
|
report(active_generation, std::current_exception());
|
|
return;
|
|
}
|
|
}
|
|
if (generation.load(std::memory_order_acquire) == active_generation)
|
|
running.store(false, std::memory_order_release);
|
|
}
|
|
};
|
|
|
|
Gallery_Frame_Clock::Gallery_Frame_Clock(
|
|
double frame_rate, Tick_Handler tick_handler,
|
|
Failure_Handler failure_handler)
|
|
: d(std::make_shared<Private>(frame_rate, std::move(tick_handler),
|
|
std::move(failure_handler))) {}
|
|
|
|
Gallery_Frame_Clock::~Gallery_Frame_Clock() { stop(); }
|
|
|
|
void Gallery_Frame_Clock::start() {
|
|
std::lock_guard lock(d->lifecycle_mutex);
|
|
if (d->running.exchange(true, std::memory_order_acq_rel)) return;
|
|
const auto generation = d->generation.fetch_add(
|
|
1, std::memory_order_acq_rel) + 1;
|
|
d->thread = std::jthread([state = d, generation](std::stop_token stop) {
|
|
state->run(stop, generation);
|
|
});
|
|
}
|
|
|
|
void Gallery_Frame_Clock::stop() noexcept {
|
|
if (!d) return;
|
|
std::jthread thread;
|
|
{
|
|
std::lock_guard lock(d->lifecycle_mutex);
|
|
if (!d->running.exchange(false, std::memory_order_acq_rel)) return;
|
|
d->thread.request_stop();
|
|
d->wake.notify_all();
|
|
thread = std::move(d->thread);
|
|
}
|
|
if (!thread.joinable()) return;
|
|
if (thread.get_id() == std::this_thread::get_id()) thread.detach();
|
|
else thread.join();
|
|
}
|
|
}
|