chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include <initializer_list>
|
||||
#include <ranges>
|
||||
#include <vector>
|
||||
|
||||
namespace helpers::container {
|
||||
|
||||
template<typename R, typename V>
|
||||
concept range_of = std::ranges::range<R> && std::same_as<std::ranges::range_value_t<R>, V>;
|
||||
|
||||
template<std::ranges::range T, std::ranges::range U>
|
||||
requires std::same_as<std::ranges::range_value_t<T>, std::ranges::range_value_t<U>> &&
|
||||
requires(T&& dst, U&& src) {
|
||||
#if __cpp_lib_containers_ranges
|
||||
dst.append_range(std::forward<U>(src));
|
||||
#else
|
||||
dst.insert(dst.end(), src.begin(), src.end());
|
||||
#endif
|
||||
}
|
||||
inline void append_range(T&& dst, U&& src) {
|
||||
#if __cpp_lib_containers_ranges
|
||||
dst.append_range(std::forward<U>(src));
|
||||
#else
|
||||
dst.insert(dst.end(), src.begin(), src.end());
|
||||
#endif
|
||||
}
|
||||
|
||||
template<std::ranges::range T>
|
||||
requires requires(T&& dst, std::initializer_list<std::ranges::range_value_t<T>>&& src) {
|
||||
#if __cpp_lib_containers_ranges
|
||||
dst.append_range(std::move(src));
|
||||
#else
|
||||
dst.insert(dst.end(), src.begin(), src.end());
|
||||
#endif
|
||||
}
|
||||
inline void append_range(T&& dst, std::initializer_list<std::ranges::range_value_t<T>>&& src) {
|
||||
append_range<T, std::initializer_list<std::ranges::range_value_t<T>>&&>(std::forward<T>(dst), std::move(src));
|
||||
}
|
||||
} // namespace helpers::container
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <ranges>
|
||||
#include <type_traits>
|
||||
|
||||
namespace helpers::future {
|
||||
namespace detail {
|
||||
template<typename T>
|
||||
struct future_value;
|
||||
|
||||
template<typename T>
|
||||
struct future_value<std::future<T>> {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
concept future_range = std::ranges::range<T> && requires {
|
||||
typename future_value<std::ranges::range_value_t<std::remove_cvref_t<T>>>::type;
|
||||
};
|
||||
|
||||
template<future_range T>
|
||||
using future_range_value_t = future_value<std::ranges::range_value_t<std::remove_cvref_t<T>>>::type;
|
||||
} // namespace detail
|
||||
|
||||
struct FailurePolicyDrainAll;
|
||||
// Stops draining after the first observed failure. This does not cancel remaining
|
||||
// work; use only when unfinished tasks do not depend on this stack frame.
|
||||
struct FailurePolicyStopOnFirstFailure;
|
||||
|
||||
template<typename T>
|
||||
concept future_failure_policy = std::same_as<T, FailurePolicyDrainAll> || std::same_as<T, FailurePolicyStopOnFirstFailure>;
|
||||
|
||||
template<future_failure_policy P = FailurePolicyDrainAll, detail::future_range T>
|
||||
requires std::same_as<detail::future_range_value_t<T>, void>
|
||||
void drain_futures(T&& futures) {
|
||||
std::exception_ptr first_exception;
|
||||
for (auto& future : futures) {
|
||||
try {
|
||||
future.get();
|
||||
} catch (...) {
|
||||
if (!first_exception) {
|
||||
first_exception = std::current_exception();
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<P, FailurePolicyStopOnFirstFailure>) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first_exception) {
|
||||
std::rethrow_exception(first_exception);
|
||||
}
|
||||
}
|
||||
|
||||
template<future_failure_policy P = FailurePolicyDrainAll, detail::future_range T, typename F>
|
||||
requires std::same_as<detail::future_range_value_t<T>, void> &&
|
||||
std::invocable<F&, size_t>
|
||||
void drain_futures(T&& futures, F&& f) {
|
||||
std::exception_ptr first_exception;
|
||||
size_t index = 0;
|
||||
for (auto& future : futures) {
|
||||
try {
|
||||
future.get();
|
||||
std::invoke(f, index);
|
||||
} catch (...) {
|
||||
if (!first_exception) {
|
||||
first_exception = std::current_exception();
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<P, FailurePolicyStopOnFirstFailure>) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
if (first_exception) {
|
||||
std::rethrow_exception(first_exception);
|
||||
}
|
||||
}
|
||||
|
||||
template<future_failure_policy P = FailurePolicyDrainAll, detail::future_range T, typename F>
|
||||
requires(!std::same_as<detail::future_range_value_t<T>, void>) &&
|
||||
std::invocable<F&, detail::future_range_value_t<T>&&>
|
||||
void drain_futures(T&& futures, F&& f) {
|
||||
std::exception_ptr first_exception;
|
||||
for (auto& future : futures) {
|
||||
try {
|
||||
std::invoke(f, future.get());
|
||||
} catch (...) {
|
||||
if (!first_exception) {
|
||||
first_exception = std::current_exception();
|
||||
}
|
||||
if constexpr (std::is_same_v<P, FailurePolicyStopOnFirstFailure>) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first_exception) {
|
||||
std::rethrow_exception(first_exception);
|
||||
}
|
||||
}
|
||||
|
||||
template<future_failure_policy P = FailurePolicyDrainAll, detail::future_range T, typename F>
|
||||
requires(!std::same_as<detail::future_range_value_t<T>, void>) &&
|
||||
std::invocable<F&, detail::future_range_value_t<T>&&, size_t>
|
||||
void drain_futures(T&& futures, F&& f) {
|
||||
std::exception_ptr first_exception;
|
||||
size_t index = 0;
|
||||
for (auto& future : futures) {
|
||||
try {
|
||||
std::invoke(f, future.get(), index);
|
||||
} catch (...) {
|
||||
if (!first_exception) {
|
||||
first_exception = std::current_exception();
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<P, FailurePolicyStopOnFirstFailure>) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
if (first_exception) {
|
||||
std::rethrow_exception(first_exception);
|
||||
}
|
||||
}
|
||||
} // namespace helpers::future
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
#include <napi.h>
|
||||
|
||||
namespace node_api::imaging {
|
||||
Napi::Value avif_encode_rgba(const Napi::CallbackInfo& info);
|
||||
Napi::Value avif_encode_rgba_batched(const Napi::CallbackInfo& info);
|
||||
Napi::Value avif_decode_rgba(const Napi::CallbackInfo& info);
|
||||
Napi::Value avif_decode_rgba_batched(const Napi::CallbackInfo& info);
|
||||
} // namespace node_api::imaging
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <napi.h>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace node_api::buffer {
|
||||
template<typename T, typename D = std::default_delete<T>>
|
||||
class UniqueBufferFinalizer {
|
||||
public:
|
||||
UniqueBufferFinalizer() noexcept = default;
|
||||
UniqueBufferFinalizer(const UniqueBufferFinalizer<T, D>& other) = delete;
|
||||
UniqueBufferFinalizer(UniqueBufferFinalizer<T, D>&& other) noexcept = default;
|
||||
UniqueBufferFinalizer(std::unique_ptr<T, D>&& ptr) noexcept : ptr(std::move(ptr)) {}
|
||||
UniqueBufferFinalizer(T* ptr) noexcept : ptr(ptr) {}
|
||||
void operator()(Napi::Env env, T* data) noexcept {}
|
||||
|
||||
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, std::unique_ptr<T, D>&& ptr, size_t size) noexcept {
|
||||
auto finalizer = UniqueBufferFinalizer(std::move(ptr));
|
||||
auto data_ptr = finalizer.ptr.get();
|
||||
return Napi::Buffer<T>::NewOrCopy(env, data_ptr, size, std::move(finalizer));
|
||||
}
|
||||
|
||||
protected:
|
||||
std::unique_ptr<T, D> ptr;
|
||||
};
|
||||
|
||||
template<typename T, typename D = std::default_delete<std::vector<T>>>
|
||||
class UniqueVecBufferFinalizer : public UniqueBufferFinalizer<std::vector<T>, D> {
|
||||
public:
|
||||
void operator()(Napi::Env env, T* data) noexcept {}
|
||||
|
||||
template<typename U>
|
||||
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, U&& data) noexcept {
|
||||
auto finalizer = UniqueVecBufferFinalizer(std::forward<U>(data));
|
||||
auto data_ptr = finalizer.ptr->data();
|
||||
auto data_size = finalizer.ptr->size();
|
||||
return Napi::Buffer<T>::NewOrCopy(env, data_ptr, data_size, std::move(finalizer));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class SharedBufferFinalizer {
|
||||
public:
|
||||
SharedBufferFinalizer() noexcept = default;
|
||||
SharedBufferFinalizer(const SharedBufferFinalizer<T>& other) noexcept = default;
|
||||
SharedBufferFinalizer(SharedBufferFinalizer<T>&& other) noexcept = default;
|
||||
SharedBufferFinalizer(const std::shared_ptr<T>& ptr) noexcept : ptr(ptr) {}
|
||||
SharedBufferFinalizer(std::shared_ptr<T>&& ptr) noexcept : ptr(std::move(ptr)) {}
|
||||
void operator()(Napi::Env env, T* data) noexcept {}
|
||||
|
||||
template<typename U>
|
||||
requires std::is_same_v<std::remove_reference_t<U>, std::shared_ptr<T>>
|
||||
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, U&& ptr, size_t size) noexcept {
|
||||
auto finalizer = SharedBufferFinalizer(std::forward<U>(ptr));
|
||||
auto data_ptr = finalizer.ptr.get();
|
||||
return Napi::Buffer<T>::NewOrCopy(env, data_ptr, size, std::move(finalizer));
|
||||
}
|
||||
|
||||
template<typename U>
|
||||
requires std::is_same_v<std::remove_reference_t<U>, std::shared_ptr<T>>
|
||||
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, U&& ptr, size_t offset, size_t size) noexcept {
|
||||
auto finalizer = SharedBufferFinalizer(std::forward<U>(ptr));
|
||||
auto data_ptr = finalizer.ptr.get();
|
||||
return Napi::Buffer<T>::NewOrCopy(env, data_ptr + offset, size, std::move(finalizer));
|
||||
}
|
||||
|
||||
protected:
|
||||
std::shared_ptr<T> ptr;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class SharedVecBufferFinalizer : public SharedBufferFinalizer<std::vector<T>> {
|
||||
public:
|
||||
void operator()(Napi::Env env, T* data) noexcept {}
|
||||
|
||||
template<typename U>
|
||||
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, U&& data) noexcept {
|
||||
auto finalizer = SharedVecBufferFinalizer(std::forward<U>(data));
|
||||
auto data_ptr = finalizer.ptr->data();
|
||||
auto data_size = finalizer.ptr->size();
|
||||
return Napi::Buffer<T>::NewOrCopy(env, data_ptr, data_size, std::move(finalizer));
|
||||
}
|
||||
|
||||
template<typename U>
|
||||
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, U&& data, size_t offset, size_t size) noexcept {
|
||||
auto finalizer = SharedVecBufferFinalizer(std::forward<U>(data));
|
||||
auto data_ptr = finalizer.ptr->data();
|
||||
return Napi::Buffer<T>::NewOrCopy(env, data_ptr + offset, size, std::move(finalizer));
|
||||
}
|
||||
};
|
||||
} // namespace node_api::buffer
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include <napi.h>
|
||||
|
||||
namespace node_api::spatial {
|
||||
Napi::Value cluster_average(const Napi::CallbackInfo& info);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <napi.h>
|
||||
|
||||
namespace node_api::splat {
|
||||
Napi::Value generate_lod(const Napi::CallbackInfo& info);
|
||||
Napi::Value split(const Napi::CallbackInfo& info);
|
||||
} // namespace node_api::splat
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <napi.h>
|
||||
#include <thread>
|
||||
#include <thread_pool.h>
|
||||
|
||||
namespace node_api::threading {
|
||||
class ThreadPool : public Napi::ObjectWrap<ThreadPool> {
|
||||
public:
|
||||
static Napi::FunctionReference Init(Napi::Env env, Napi::Object exports);
|
||||
|
||||
ThreadPool(const Napi::CallbackInfo& info);
|
||||
::threading::ThreadPool& impl() noexcept;
|
||||
|
||||
ThreadPool() = delete;
|
||||
|
||||
private:
|
||||
static size_t calc_thread_count(const Napi::CallbackInfo& info);
|
||||
Napi::Value thread_count(const Napi::CallbackInfo& info);
|
||||
Napi::Value task_count(const Napi::CallbackInfo& info);
|
||||
|
||||
::threading::ThreadPool impl_;
|
||||
};
|
||||
} // namespace node_api::threading
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include <napi.h>
|
||||
|
||||
namespace node_api::imaging {
|
||||
Napi::Value webp_encode_rgba_lossless(const Napi::CallbackInfo& info);
|
||||
Napi::Value webp_encode_rgba(const Napi::CallbackInfo& info);
|
||||
Napi::Value webp_decode_rgba(const Napi::CallbackInfo& info);
|
||||
} // namespace node_api::imaging
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <eigen3/Eigen/Dense>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace splat {
|
||||
class SH {
|
||||
public:
|
||||
SH() noexcept;
|
||||
SH(size_t size);
|
||||
SH(const SH& other);
|
||||
SH(SH&& other) noexcept;
|
||||
|
||||
SH& operator=(const SH& other);
|
||||
SH& operator=(SH&& other) noexcept;
|
||||
|
||||
void swap(SH& other) noexcept;
|
||||
|
||||
SH& set_zero() noexcept;
|
||||
SH& add_multiplied(const SH& other, float scalar) noexcept;
|
||||
|
||||
const float& operator[](size_t index) const;
|
||||
float& operator[](size_t index);
|
||||
|
||||
float* data() noexcept;
|
||||
size_t size() const noexcept;
|
||||
|
||||
~SH() noexcept;
|
||||
|
||||
private:
|
||||
void reset() noexcept;
|
||||
|
||||
size_t size_;
|
||||
std::unique_ptr<float[]> ptr;
|
||||
};
|
||||
|
||||
struct Gaussian {
|
||||
Eigen::Vector3f mean;
|
||||
Eigen::Vector3f scale;
|
||||
Eigen::Vector4f rotation;
|
||||
Eigen::Matrix3f covariance;
|
||||
SH sh;
|
||||
float opacity;
|
||||
Eigen::AlignedBox3f bounding_box;
|
||||
|
||||
void compute_covariance();
|
||||
void decompose_covariance();
|
||||
void compute_bounding_box(float k = 3.0f);
|
||||
float area() const;
|
||||
};
|
||||
|
||||
struct Splat {
|
||||
std::vector<Gaussian> gaussians;
|
||||
Eigen::AlignedBox3f bounding_box;
|
||||
|
||||
void compute_bounding_box();
|
||||
void compute_compact_bounding_box();
|
||||
};
|
||||
} // namespace splat
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include <splat/splat.h>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
namespace splat::block {
|
||||
namespace detail {
|
||||
std::vector<Splat> split(const Splat& splat, size_t max_block_size);
|
||||
} // namespace detail
|
||||
|
||||
template<typename T>
|
||||
requires std::is_same_v<std::remove_reference_t<T>, Splat>
|
||||
std::vector<Splat> split(T&& input, double precision) {
|
||||
auto max_block_size = static_cast<size_t>(static_cast<double>(input.gaussians.size()) * precision);
|
||||
return detail::split(input, max_block_size);
|
||||
}
|
||||
} // namespace splat::block
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include <container_helpers.h>
|
||||
#include <cstdint>
|
||||
#include <splat/splat.h>
|
||||
#include <vector>
|
||||
|
||||
namespace splat::lod {
|
||||
namespace detail {
|
||||
Splat reduce_gaussians(size_t id, const Splat& splat, size_t target_count, float scale_boost, size_t max_step);
|
||||
}
|
||||
|
||||
struct SplatLod {
|
||||
std::vector<Splat> splats;
|
||||
std::vector<uint32_t> levels;
|
||||
};
|
||||
|
||||
struct SplatLevelParameters {
|
||||
float precision;
|
||||
float scale_boost;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
requires requires(T) {
|
||||
requires helpers::container::range_of<T, SplatLevelParameters>;
|
||||
}
|
||||
inline void generate_lod(size_t id, SplatLod& lod, T&& parameters, size_t min_size, size_t max_step) {
|
||||
auto base_count = lod.splats.back().gaussians.size();
|
||||
for (auto& parameter : parameters) {
|
||||
auto target_count = std::max<size_t>(static_cast<size_t>(std::floor(static_cast<double>(base_count) * parameter.precision)), 1);
|
||||
auto& current = lod.splats.back();
|
||||
if (current.gaussians.size() > min_size && target_count < current.gaussians.size()) {
|
||||
auto generated = detail::reduce_gaussians(
|
||||
id,
|
||||
current,
|
||||
target_count,
|
||||
parameter.scale_boost,
|
||||
max_step);
|
||||
lod.splats.push_back(std::move(generated));
|
||||
}
|
||||
lod.levels.push_back(lod.splats.size() - 1);
|
||||
}
|
||||
}
|
||||
} // namespace splat::lod
|
||||
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <concepts>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <shared_mutex>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace threading {
|
||||
class ThreadPool {
|
||||
public:
|
||||
ThreadPool(size_t thread_count = std::thread::hardware_concurrency());
|
||||
ThreadPool(ThreadPool&& other) noexcept;
|
||||
|
||||
template<typename F, typename... Args,
|
||||
typename ReturnType = std::invoke_result_t<std::decay_t<F>, std::decay_t<Args>...>>
|
||||
requires std::invocable<std::decay_t<F>, std::decay_t<Args>...>
|
||||
std::future<ReturnType> submit_task(F&& f, Args&&... args);
|
||||
size_t thread_count() const noexcept;
|
||||
size_t task_count() const noexcept;
|
||||
ThreadPool& operator=(ThreadPool&& other) noexcept;
|
||||
void stop() noexcept;
|
||||
~ThreadPool() noexcept;
|
||||
|
||||
ThreadPool(const ThreadPool& other) = delete;
|
||||
ThreadPool& operator=(const ThreadPool& other) = delete;
|
||||
|
||||
private:
|
||||
#ifdef __cpp_lib_move_only_function
|
||||
using Task = std::move_only_function<void()>;
|
||||
#else
|
||||
using Task = std::function<void()>;
|
||||
#endif
|
||||
|
||||
struct Worker {
|
||||
std::atomic<bool> stopped { false };
|
||||
std::thread thread;
|
||||
|
||||
void stop() noexcept;
|
||||
void clean_up() noexcept;
|
||||
~Worker() noexcept;
|
||||
};
|
||||
|
||||
struct State {
|
||||
std::vector<std::unique_ptr<Worker>> workers;
|
||||
std::queue<Task> tasks;
|
||||
mutable std::shared_mutex queue_mutex;
|
||||
mutable std::shared_mutex worker_mutex;
|
||||
std::condition_variable_any cv;
|
||||
std::atomic<bool> stopped { false };
|
||||
|
||||
void stop() noexcept;
|
||||
~State() noexcept;
|
||||
};
|
||||
|
||||
static void worker_loop(State* state, Worker* worker);
|
||||
|
||||
std::unique_ptr<State> state;
|
||||
};
|
||||
|
||||
template<typename F, typename... Args, typename ReturnType>
|
||||
requires std::invocable<std::decay_t<F>, std::decay_t<Args>...>
|
||||
std::future<ReturnType> ThreadPool::submit_task(F&& f, Args&&... args) {
|
||||
auto* state = this->state.get();
|
||||
if (state == nullptr || state->stopped.load(std::memory_order_relaxed)) {
|
||||
throw std::runtime_error("Cannot submit tasks to stopped thread pool");
|
||||
}
|
||||
#ifdef __cpp_lib_move_only_function
|
||||
auto promise = std::promise<ReturnType>();
|
||||
auto future = promise.get_future();
|
||||
{
|
||||
auto lk = std::lock_guard(state->queue_mutex);
|
||||
if (state->stopped.load(std::memory_order_relaxed)) {
|
||||
throw std::runtime_error("Cannot submit tasks to stopped thread pool");
|
||||
}
|
||||
state->tasks.emplace([f = std::forward<F>(f), ... args = std::forward<Args>(args), promise = std::move(promise)]() mutable noexcept -> void {
|
||||
try {
|
||||
if constexpr (std::is_void_v<ReturnType>) {
|
||||
std::invoke(std::move(f), std::move(args)...);
|
||||
promise.set_value();
|
||||
} else {
|
||||
promise.set_value(std::invoke(std::move(f), std::move(args)...));
|
||||
}
|
||||
} catch (...) {
|
||||
promise.set_exception(std::current_exception());
|
||||
}
|
||||
});
|
||||
}
|
||||
#else
|
||||
auto promise = std::make_shared<std::promise<ReturnType>>();
|
||||
auto future = promise->get_future();
|
||||
{
|
||||
auto lk = std::lock_guard(state->queue_mutex);
|
||||
if (state->stopped.load(std::memory_order_relaxed)) {
|
||||
throw std::runtime_error("Cannot submit tasks to stopped thread pool");
|
||||
}
|
||||
state->tasks.emplace([f = std::forward<F>(f), ... args = std::forward<Args>(args), promise = promise]() noexcept -> void {
|
||||
try {
|
||||
if constexpr (std::is_void_v<ReturnType>) {
|
||||
std::invoke(std::move(f), std::move(args)...);
|
||||
promise->set_value();
|
||||
} else {
|
||||
promise->set_value(std::invoke(std::move(f), std::move(args)...));
|
||||
}
|
||||
} catch (...) {
|
||||
promise->set_exception(std::current_exception());
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
state->cv.notify_one();
|
||||
return future;
|
||||
}
|
||||
} // namespace threading
|
||||
Reference in New Issue
Block a user