Harden app runtime and thin export shell

This commit is contained in:
2026-06-17 18:15:54 +02:00
parent bc9ba75e49
commit 04a1c5d0b1
13 changed files with 426 additions and 206 deletions

View File

@@ -180,7 +180,6 @@ set(PP_PANOPAINTER_APP_SOURCES
src/legacy_document_open_services.h src/legacy_document_open_services.h
src/legacy_document_session_services.cpp src/legacy_document_session_services.cpp
src/legacy_document_session_services.h src/legacy_document_session_services.h
${PP_PLATFORM_WEB_SOURCES}
src/version.cpp src/version.cpp
) )

View File

@@ -18,6 +18,23 @@ agent or engineer to remove them without reconstructing context from chat.
## Reductions ## Reductions
- 2026-06-17: `DEBT-0003` was narrowed again. `src/app_runtime.cpp` and
`src/app_runtime.h` now synchronize render/UI worker running state, reject
cross-thread work once those workers stop, and drain queued work through
explicit runtime-owned post/shutdown semantics consumed by
`src/legacy_app_runtime_shell_services.cpp`; broader singleton reach, app
shell ownership, and remaining runtime service adoption still remain.
- 2026-06-17: `DEBT-0017` was narrowed again.
`cmake/PanoPainterSources.cmake` no longer compiles
`${PP_PLATFORM_WEB_SOURCES}` into `PP_PANOPAINTER_APP_SOURCES`, so root app
source ownership no longer mixes live Web platform implementation files into
the retained app group; broader platform entrypoint/package cleanup remains.
- 2026-06-17: `DEBT-0043` was narrowed again.
`src/legacy_canvas_document_io_services.cpp` now delegates the live
equirectangular export family through `src/legacy_document_export_services.*`
instead of carrying that orchestration inline with the broader canvas
document-I/O shell; retained export directory creation, Web handoff, and
remaining legacy `Canvas` export execution still remain.
- 2026-06-17: `DEBT-0003` was narrowed again. - 2026-06-17: `DEBT-0003` was narrowed again.
`src/platform_windows/windows_runtime_flow.*` now owns the live Win32 `src/platform_windows/windows_runtime_flow.*` now owns the live Win32
startup/session composition flow, including the bound app/session preflight startup/session composition flow, including the bound app/session preflight

View File

@@ -76,12 +76,12 @@ Current conclusion:
debt. debt.
- Renderer API contracts exist, but retained OpenGL resource classes still leak - Renderer API contracts exist, but retained OpenGL resource classes still leak
into app/UI/document code. into app/UI/document code.
- `AppRuntime` is a useful step toward owned queues, but thread affinity and - `AppRuntime` now owns synchronized running flags and explicit
shutdown safety are still expressed mostly by convention, mutable booleans, same-thread/post-reject queue behavior, but broader app/runtime singleton
and singleton call sites. reach and retained shell ownership still remain.
- Platform extraction improved substantially, but CMake and entrypoint cleanup - Platform extraction improved substantially and the root app source group no
are not complete. In particular, Web platform sources still appear inside longer compiles Web platform sources directly, but broader CMake and
`PP_PANOPAINTER_APP_SOURCES`, even though `pp_platform_web` also exists. entrypoint cleanup are not complete.
## Target Architecture ## Target Architecture

View File

@@ -55,11 +55,9 @@ Key facts:
- Raw `Node*` and callback captures remain a dominant UI lifetime risk. - Raw `Node*` and callback captures remain a dominant UI lifetime risk.
- `RTT`, `Texture2D`, `Shape`, `Shader`, `Font`, and `CanvasLayer` still route - `RTT`, `Texture2D`, `Shape`, `Shader`, `Font`, and `CanvasLayer` still route
render work through `App::I` queues. render work through `App::I` queues.
- `AppRuntime` uses `std::jthread`, which is progress, but queue ownership, - `AppRuntime` now owns synchronized running flags plus explicit post/reject,
running flags, thread affinity, and shutdown contracts are not yet component same-thread execution, and queue-drain behavior, but broader singleton reach
contracts. and app-shell ownership remain.
- `PP_PANOPAINTER_APP_SOURCES` still includes `${PP_PLATFORM_WEB_SOURCES}`,
which should be removed in favor of concrete `pp_platform_web` ownership.
## Parallel Assignment Rules ## Parallel Assignment Rules

View File

@@ -336,19 +336,19 @@ public:
bool is_render_thread() bool is_render_thread()
{ {
return runtime_.is_render_thread(); return runtime().is_render_thread();
} }
template<typename T> template<typename T>
std::future<void> render_task_async(T task, bool unique = false) std::future<void> render_task_async(T task, bool unique = false)
{ {
return runtime_.render_task_async(std::move(task), unique); return runtime().render_task_async(std::move(task), unique);
} }
template<typename T> template<typename T>
void render_task(T task) void render_task(T task)
{ {
runtime_.render_task(std::move(task)); runtime().render_task(std::move(task));
} }
void render_sync() void render_sync()
@@ -367,27 +367,27 @@ public:
bool is_ui_thread() bool is_ui_thread()
{ {
return runtime_.is_ui_thread(); return runtime().is_ui_thread();
} }
template<typename T> template<typename T>
std::future<void> ui_task_async(T task, bool unique = false) std::future<void> ui_task_async(T task, bool unique = false)
{ {
return runtime_.ui_task_async(std::move(task), unique); return runtime().ui_task_async(std::move(task), unique);
} }
template<typename T> template<typename T>
void ui_task(T task) void ui_task(T task)
{ {
runtime_.ui_task(std::move(task)); runtime().ui_task(std::move(task));
if (runtime_.request_redraw()) if (runtime().request_redraw())
redraw = true; redraw = true;
runtime_.clear_request_redraw(); runtime().clear_request_redraw();
} }
void ui_sync() void ui_sync()
{ {
runtime_.ui_sync(); runtime().ui_sync();
} }
private: private:

View File

@@ -19,6 +19,17 @@ struct AppTaskDispatchPlan {
bool reject_unsafe_cross_thread_dispatch = false; bool reject_unsafe_cross_thread_dispatch = false;
}; };
struct AppRuntimeTaskDispatchPlan {
bool execute_immediately = false;
bool queue_task = false;
bool remove_matching_unique_task = false;
bool notify_worker = false;
bool wait_for_completion = false;
bool request_redraw = false;
bool reject_unsafe_cross_thread_dispatch = false;
bool reject_stopped_worker_dispatch = false;
};
struct AppAsyncRedrawPlan { struct AppAsyncRedrawPlan {
bool set_redraw = true; bool set_redraw = true;
bool notify_ui = true; bool notify_ui = true;
@@ -93,6 +104,31 @@ struct AppThreadStopPlan {
}; };
} }
[[nodiscard]] constexpr AppRuntimeTaskDispatchPlan plan_app_runtime_task_dispatch(
bool already_on_target_thread,
bool unique,
std::size_t queued_task_count,
bool worker_running,
bool wait_for_completion,
bool request_redraw_after_dispatch,
bool reject_unsafe_cross_thread_dispatch = false) noexcept
{
const bool queue_task = !already_on_target_thread
&& worker_running
&& !reject_unsafe_cross_thread_dispatch;
return AppRuntimeTaskDispatchPlan {
.execute_immediately = already_on_target_thread,
.queue_task = queue_task,
.remove_matching_unique_task = queue_task && unique && queued_task_count > 0U,
.notify_worker = queue_task,
.wait_for_completion = queue_task && wait_for_completion,
.request_redraw = (already_on_target_thread || queue_task) && request_redraw_after_dispatch,
.reject_unsafe_cross_thread_dispatch = !already_on_target_thread
&& reject_unsafe_cross_thread_dispatch,
.reject_stopped_worker_dispatch = !already_on_target_thread && !worker_running,
};
}
[[nodiscard]] constexpr AppAsyncRedrawPlan plan_app_async_redraw() noexcept [[nodiscard]] constexpr AppAsyncRedrawPlan plan_app_async_redraw() noexcept
{ {
return AppAsyncRedrawPlan {}; return AppAsyncRedrawPlan {};

View File

@@ -3,6 +3,34 @@
#include "app.h" #include "app.h"
namespace {
void execute_render_worker_task(AppTask& task) noexcept
{
try
{
task();
}
catch (...)
{
LOG("render worker task failed");
}
}
void execute_ui_worker_task(AppTask& task) noexcept
{
try
{
task();
}
catch (...)
{
LOG("ui worker task failed");
}
}
} // namespace
AppRuntime::AppRuntime() AppRuntime::AppRuntime()
: prepared_file_worker_([this](std::stop_token stop_token) : prepared_file_worker_([this](std::stop_token stop_token)
{ {
@@ -23,11 +51,13 @@ AppRuntime::~AppRuntime()
bool AppRuntime::is_render_thread() const noexcept bool AppRuntime::is_render_thread() const noexcept
{ {
std::lock_guard<std::mutex> lock(render_task_mutex_);
return std::this_thread::get_id() == render_thread_id_; return std::this_thread::get_id() == render_thread_id_;
} }
bool AppRuntime::is_ui_thread() const noexcept bool AppRuntime::is_ui_thread() const noexcept
{ {
std::lock_guard<std::mutex> lock(ui_task_mutex_);
return std::this_thread::get_id() == ui_thread_id_; return std::this_thread::get_id() == ui_thread_id_;
} }
@@ -84,6 +114,174 @@ void AppRuntime::drain_main_thread_tasks()
} }
} }
std::future<void> AppRuntime::render_task_async(AppTask task, bool unique)
{
auto future = task.get_future();
const auto on_render_thread = is_render_thread();
const auto running = render_running_.load(std::memory_order_acquire);
const auto dispatch = pp::app::plan_app_runtime_task_dispatch(
on_render_thread,
unique,
0U,
running,
false,
false);
if (dispatch.execute_immediately)
{
execute_render_worker_task(task);
return future;
}
if (!dispatch.queue_task)
return future;
bool notify_worker = false;
{
std::lock_guard<std::mutex> lock(render_task_mutex_);
if (!render_running_.load(std::memory_order_acquire))
return future;
const auto queue_dispatch = pp::app::plan_app_runtime_task_dispatch(
false,
unique,
render_tasklist_.size(),
true,
false,
false);
if (queue_dispatch.remove_matching_unique_task)
{
render_tasklist_.erase(
std::remove_if(render_tasklist_.begin(), render_tasklist_.end(),
[id = task.task_id](AppTask const& t) { return t.task_id == id; }),
render_tasklist_.end());
}
render_tasklist_.push_back(std::move(task));
notify_worker = queue_dispatch.notify_worker;
}
if (notify_worker)
render_cv_.notify_all();
return future;
}
void AppRuntime::render_task(AppTask task)
{
auto future = task.get_future();
const auto on_render_thread = is_render_thread();
const auto running = render_running_.load(std::memory_order_acquire);
const auto dispatch = pp::app::plan_app_runtime_task_dispatch(
on_render_thread,
false,
0U,
running,
true,
false);
if (dispatch.execute_immediately)
{
execute_render_worker_task(task);
}
else if (dispatch.queue_task)
{
bool notify_worker = false;
{
std::lock_guard<std::mutex> lock(render_task_mutex_);
if (!render_running_.load(std::memory_order_acquire))
return;
render_tasklist_.push_back(std::move(task));
notify_worker = dispatch.notify_worker;
}
if (notify_worker)
render_cv_.notify_all();
}
if (dispatch.wait_for_completion)
future.get();
}
std::future<void> AppRuntime::ui_task_async(AppTask task, bool unique)
{
auto future = task.get_future();
const auto on_ui_thread = is_ui_thread();
const auto running = ui_running_.load(std::memory_order_acquire);
const auto dispatch = pp::app::plan_app_runtime_task_dispatch(
on_ui_thread,
unique,
0U,
running,
false,
false);
if (dispatch.execute_immediately)
{
execute_ui_worker_task(task);
return future;
}
if (!dispatch.queue_task)
return future;
bool notify_worker = false;
{
std::lock_guard<std::mutex> lock(ui_task_mutex_);
if (!ui_running_.load(std::memory_order_acquire))
return future;
const auto queue_dispatch = pp::app::plan_app_runtime_task_dispatch(
false,
unique,
ui_tasklist_.size(),
true,
false,
false);
if (queue_dispatch.remove_matching_unique_task)
{
ui_tasklist_.erase(
std::remove_if(ui_tasklist_.begin(), ui_tasklist_.end(),
[id = task.task_id](AppTask const& t) { return t.task_id == id; }),
ui_tasklist_.end());
}
ui_tasklist_.push_back(std::move(task));
notify_worker = queue_dispatch.notify_worker;
}
if (notify_worker)
ui_cv_.notify_all();
return future;
}
void AppRuntime::ui_task(AppTask task)
{
auto future = task.get_future();
const auto on_ui_thread = is_ui_thread();
const auto running = ui_running_.load(std::memory_order_acquire);
const auto dispatch = pp::app::plan_app_runtime_task_dispatch(
on_ui_thread,
false,
0U,
running,
true,
true);
if (dispatch.execute_immediately)
{
execute_ui_worker_task(task);
}
else if (dispatch.queue_task)
{
bool notify_worker = false;
{
std::lock_guard<std::mutex> lock(ui_task_mutex_);
if (!ui_running_.load(std::memory_order_acquire))
return;
ui_tasklist_.push_back(std::move(task));
notify_worker = dispatch.notify_worker;
}
if (notify_worker)
ui_cv_.notify_all();
}
if (dispatch.wait_for_completion)
future.get();
if (dispatch.request_redraw)
request_redraw_.store(true, std::memory_order_release);
}
void AppRuntime::prepared_file_worker_main(std::stop_token stop_token) void AppRuntime::prepared_file_worker_main(std::stop_token stop_token)
{ {
for (;;) for (;;)
@@ -177,14 +375,17 @@ void AppRuntime::canvas_async_worker_stop()
void AppRuntime::render_thread_tick(App& app) void AppRuntime::render_thread_tick(App& app)
{ {
static uint32_t count = 0; static uint32_t count = 0;
render_thread_id_ = std::this_thread::get_id(); {
std::lock_guard<std::mutex> lock(render_task_mutex_);
render_thread_id_ = std::this_thread::get_id();
}
std::deque<AppTask> working_list; std::deque<AppTask> working_list;
pp::app::AppQueueDrainPlan drain_plan; pp::app::AppQueueDrainPlan drain_plan;
{ {
std::unique_lock<std::mutex> lock(render_task_mutex_); std::unique_lock<std::mutex> lock(render_task_mutex_);
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size()); drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size());
render_running_ = drain_plan.mark_running; render_running_.store(drain_plan.mark_running, std::memory_order_release);
if (!drain_plan.drain_tasks) if (!drain_plan.drain_tasks)
return; return;
working_list = std::move(render_tasklist_); working_list = std::move(render_tasklist_);
@@ -196,7 +397,7 @@ void AppRuntime::render_thread_tick(App& app)
while (!working_list.empty()) while (!working_list.empty())
{ {
count++; count++;
working_list.front()(); execute_render_worker_task(working_list.front());
working_list.pop_front(); working_list.pop_front();
} }
app.async_end(); app.async_end();
@@ -207,39 +408,46 @@ void AppRuntime::render_thread_main(App& app, std::stop_token stop_token)
{ {
BT_SetTerminate(); BT_SetTerminate();
render_thread_id_ = std::this_thread::get_id(); {
render_running_ = pp::app::plan_app_thread_start().mark_running; std::lock_guard<std::mutex> lock(render_task_mutex_);
while (render_running_ && !stop_token.stop_requested()) render_thread_id_ = std::this_thread::get_id();
}
render_running_.store(pp::app::plan_app_thread_start().mark_running, std::memory_order_release);
for (;;)
{ {
std::deque<AppTask> working_list; std::deque<AppTask> working_list;
pp::app::AppQueueDrainPlan drain_plan;
{ {
std::unique_lock<std::mutex> lock(render_task_mutex_); std::unique_lock<std::mutex> lock(render_task_mutex_);
render_cv_.wait(lock, [this, &stop_token] render_cv_.wait(lock, [this, &stop_token]
{ {
return stop_token.stop_requested() || !render_running_ || !render_tasklist_.empty(); return stop_token.stop_requested() || !render_running_.load(std::memory_order_acquire) || !render_tasklist_.empty();
}); });
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size()); if (render_tasklist_.empty())
{
if (stop_token.stop_requested() || !render_running_.load(std::memory_order_acquire))
break;
continue;
}
working_list = std::move(render_tasklist_); working_list = std::move(render_tasklist_);
} }
if (drain_plan.wrap_in_render_context) app.async_start();
while (!working_list.empty())
{ {
app.async_start(); execute_render_worker_task(working_list.front());
while (!working_list.empty()) working_list.pop_front();
{
working_list.front()();
working_list.pop_front();
}
app.async_end();
} }
app.async_end();
} }
} }
void AppRuntime::ui_thread_tick(App& app) void AppRuntime::ui_thread_tick(App& app)
{ {
ui_thread_id_ = std::this_thread::get_id(); {
std::lock_guard<std::mutex> lock(ui_task_mutex_);
ui_thread_id_ = std::this_thread::get_id();
}
std::deque<AppTask> working_list; std::deque<AppTask> working_list;
pp::app::AppUiTickPlan tick_plan; pp::app::AppUiTickPlan tick_plan;
@@ -247,7 +455,7 @@ void AppRuntime::ui_thread_tick(App& app)
{ {
std::unique_lock<std::mutex> lock(ui_task_mutex_); std::unique_lock<std::mutex> lock(ui_task_mutex_);
tick_plan = pp::app::plan_app_ui_thread_tick(ui_tasklist_.size(), app.redraw); tick_plan = pp::app::plan_app_ui_thread_tick(ui_tasklist_.size(), app.redraw);
ui_running_ = tick_plan.mark_running; ui_running_.store(tick_plan.mark_running, std::memory_order_release);
working_list = std::move(ui_tasklist_); working_list = std::move(ui_tasklist_);
} }
@@ -255,7 +463,7 @@ void AppRuntime::ui_thread_tick(App& app)
{ {
while (!working_list.empty()) while (!working_list.empty())
{ {
working_list.front()(); execute_ui_worker_task(working_list.front());
working_list.pop_front(); working_list.pop_front();
} }
} }
@@ -282,8 +490,11 @@ void AppRuntime::ui_thread_main(App& app, std::stop_token stop_token)
{ {
BT_SetTerminate(); BT_SetTerminate();
ui_thread_id_ = std::this_thread::get_id(); {
ui_running_ = pp::app::plan_app_thread_start().mark_running; std::lock_guard<std::mutex> lock(ui_task_mutex_);
ui_thread_id_ = std::this_thread::get_id();
}
ui_running_.store(pp::app::plan_app_thread_start().mark_running, std::memory_order_release);
app.attach_ui_thread(); app.attach_ui_thread();
@@ -295,7 +506,7 @@ void AppRuntime::ui_thread_main(App& app, std::stop_token stop_token)
float t_fps_counter = 0; float t_fps_counter = 0;
float t_reloader = 0; float t_reloader = 0;
int rendered_frames = 0; int rendered_frames = 0;
while (ui_running_ && !stop_token.stop_requested()) for (;;)
{ {
std::deque<AppTask> working_list; std::deque<AppTask> working_list;
@@ -303,16 +514,24 @@ void AppRuntime::ui_thread_main(App& app, std::stop_token stop_token)
std::unique_lock<std::mutex> lock(ui_task_mutex_); std::unique_lock<std::mutex> lock(ui_task_mutex_);
ui_cv_.wait_for(lock, std::chrono::milliseconds(app.idle_ms), [this, &stop_token] ui_cv_.wait_for(lock, std::chrono::milliseconds(app.idle_ms), [this, &stop_token]
{ {
return stop_token.stop_requested() || !ui_running_ || !ui_tasklist_.empty(); return stop_token.stop_requested() || !ui_running_.load(std::memory_order_acquire) || !ui_tasklist_.empty();
}); });
working_list = std::move(ui_tasklist_); if (ui_tasklist_.empty())
{
if (stop_token.stop_requested() || !ui_running_.load(std::memory_order_acquire))
break;
}
else
{
working_list = std::move(ui_tasklist_);
}
} }
if (!working_list.empty()) if (!working_list.empty())
{ {
while (!working_list.empty()) while (!working_list.empty())
{ {
working_list.front()(); execute_ui_worker_task(working_list.front());
working_list.pop_front(); working_list.pop_front();
} }
} }
@@ -383,14 +602,14 @@ void AppRuntime::render_thread_start(App& app)
{ {
render_thread_main(app, stop_token); render_thread_main(app, stop_token);
}); });
render_running_ = plan.mark_running; render_running_.store(plan.mark_running, std::memory_order_release);
} }
void AppRuntime::render_thread_stop() void AppRuntime::render_thread_stop()
{ {
const auto plan = pp::app::plan_app_thread_stop(render_thread_.joinable()); const auto plan = pp::app::plan_app_thread_stop(render_thread_.joinable());
if (plan.mark_not_running) if (plan.mark_not_running)
render_running_ = false; render_running_.store(false, std::memory_order_release);
if (plan.join_thread) if (plan.join_thread)
render_thread_.request_stop(); render_thread_.request_stop();
if (plan.notify_worker) if (plan.notify_worker)
@@ -407,14 +626,14 @@ void AppRuntime::ui_thread_start(App& app)
{ {
ui_thread_main(app, stop_token); ui_thread_main(app, stop_token);
}); });
ui_running_ = plan.mark_running; ui_running_.store(plan.mark_running, std::memory_order_release);
} }
void AppRuntime::ui_thread_stop() void AppRuntime::ui_thread_stop()
{ {
const auto plan = pp::app::plan_app_thread_stop(ui_thread_.joinable()); const auto plan = pp::app::plan_app_thread_stop(ui_thread_.joinable());
if (plan.mark_not_running) if (plan.mark_not_running)
ui_running_ = false; ui_running_.store(false, std::memory_order_release);
if (plan.join_thread) if (plan.join_thread)
ui_thread_.request_stop(); ui_thread_.request_stop();
if (plan.notify_worker) if (plan.notify_worker)

View File

@@ -2,6 +2,7 @@
#include "app_core/app_thread.h" #include "app_core/app_thread.h"
#include <atomic>
#include <algorithm> #include <algorithm>
#include <condition_variable> #include <condition_variable>
#include <cstddef> #include <cstddef>
@@ -39,8 +40,8 @@ public:
[[nodiscard]] bool is_render_thread() const noexcept; [[nodiscard]] bool is_render_thread() const noexcept;
[[nodiscard]] bool is_ui_thread() const noexcept; [[nodiscard]] bool is_ui_thread() const noexcept;
[[nodiscard]] bool request_redraw() const noexcept { return request_redraw_; } [[nodiscard]] bool request_redraw() const noexcept { return request_redraw_.load(std::memory_order_acquire); }
void clear_request_redraw() noexcept { request_redraw_ = false; } void clear_request_redraw() noexcept { request_redraw_.store(false, std::memory_order_release); }
void notify_render_worker() noexcept; void notify_render_worker() noexcept;
void notify_ui_worker() noexcept; void notify_ui_worker() noexcept;
@@ -49,6 +50,11 @@ public:
void main_thread_task(std::packaged_task<void()> task); void main_thread_task(std::packaged_task<void()> task);
void drain_main_thread_tasks(); void drain_main_thread_tasks();
std::future<void> render_task_async(AppTask task, bool unique = false);
void render_task(AppTask task);
std::future<void> ui_task_async(AppTask task, bool unique = false);
void ui_task(AppTask task);
void render_thread_tick(App& app); void render_thread_tick(App& app);
void render_thread_main(App& app, std::stop_token stop_token); void render_thread_main(App& app, std::stop_token stop_token);
void render_thread_start(App& app); void render_thread_start(App& app);
@@ -62,68 +68,13 @@ public:
template<typename T> template<typename T>
std::future<void> render_task_async(T task, bool unique = false) std::future<void> render_task_async(T task, bool unique = false)
{ {
AppTask pt(task); return render_task_async(AppTask(std::move(task)), unique);
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_render_thread(),
unique,
0U,
render_running_,
false,
false);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(render_task_mutex_);
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
false,
unique,
render_tasklist_.size(),
render_running_,
false,
false);
if (queue_dispatch.remove_matching_unique_task)
render_tasklist_.erase(std::remove_if(render_tasklist_.begin(), render_tasklist_.end(),
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), render_tasklist_.end());
render_tasklist_.push_back(std::move(pt));
}
if (dispatch.notify_worker)
render_cv_.notify_all();
}
return f;
} }
template<typename T> template<typename T>
void render_task(T task) void render_task(T task)
{ {
AppTask pt(task); render_task(AppTask(std::move(task)));
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_render_thread(),
false,
0U,
render_running_,
true,
false);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(render_task_mutex_);
render_tasklist_.push_back(std::move(pt));
}
if (dispatch.notify_worker)
render_cv_.notify_all();
}
if (dispatch.wait_for_completion)
f.get();
} }
void render_sync() void render_sync()
@@ -134,70 +85,13 @@ public:
template<typename T> template<typename T>
std::future<void> ui_task_async(T task, bool unique = false) std::future<void> ui_task_async(T task, bool unique = false)
{ {
AppTask pt(task); return ui_task_async(AppTask(std::move(task)), unique);
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_ui_thread(),
unique,
0U,
ui_running_,
false,
false);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(ui_task_mutex_);
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
false,
unique,
ui_tasklist_.size(),
ui_running_,
false,
false);
if (queue_dispatch.remove_matching_unique_task)
ui_tasklist_.erase(std::remove_if(ui_tasklist_.begin(), ui_tasklist_.end(),
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), ui_tasklist_.end());
ui_tasklist_.push_back(std::move(pt));
}
if (dispatch.notify_worker)
ui_cv_.notify_all();
}
return f;
} }
template<typename T> template<typename T>
void ui_task(T task) void ui_task(T task)
{ {
AppTask pt(task); ui_task(AppTask(std::move(task)));
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_ui_thread(),
false,
0U,
ui_running_,
true,
true);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(ui_task_mutex_);
ui_tasklist_.push_back(std::move(pt));
}
if (dispatch.notify_worker)
ui_cv_.notify_all();
}
if (dispatch.wait_for_completion)
f.get();
if (dispatch.request_redraw)
request_redraw_ = true;
} }
void ui_sync() void ui_sync()
@@ -227,17 +121,17 @@ private:
std::mutex main_thread_task_mutex_; std::mutex main_thread_task_mutex_;
std::deque<AppTask> render_tasklist_; std::deque<AppTask> render_tasklist_;
std::mutex render_task_mutex_; mutable std::mutex render_task_mutex_;
std::condition_variable render_cv_; std::condition_variable render_cv_;
std::jthread render_thread_; std::jthread render_thread_;
std::thread::id render_thread_id_; std::thread::id render_thread_id_;
bool render_running_ = false; std::atomic_bool render_running_ = false;
std::deque<AppTask> ui_tasklist_; std::deque<AppTask> ui_tasklist_;
std::mutex ui_task_mutex_; mutable std::mutex ui_task_mutex_;
std::condition_variable ui_cv_; std::condition_variable ui_cv_;
std::jthread ui_thread_; std::jthread ui_thread_;
std::thread::id ui_thread_id_; std::thread::id ui_thread_id_;
bool ui_running_ = false; std::atomic_bool ui_running_ = false;
bool request_redraw_ = false; std::atomic_bool request_redraw_ = false;
}; };

View File

@@ -230,7 +230,7 @@ void App::async_redraw()
if (plan.set_redraw) if (plan.set_redraw)
redraw = true; redraw = true;
if (plan.notify_ui) if (plan.notify_ui)
runtime_.notify_ui_worker(); runtime().notify_ui_worker();
} }
void App::async_end() void App::async_end()
@@ -370,5 +370,5 @@ void App::rec_loop()
void App::render_thread_tick() void App::render_thread_tick()
{ {
runtime_.render_thread_tick(*this); runtime().render_thread_tick(*this);
} }

View File

@@ -4,6 +4,7 @@
#include "canvas.h" #include "canvas.h"
#include "app.h" #include "app.h"
#include "legacy_canvas_draw_merge_services.h" #include "legacy_canvas_draw_merge_services.h"
#include "legacy_document_export_services.h"
#include "legacy_ui_gl_dispatch.h" #include "legacy_ui_gl_dispatch.h"
#include "legacy_ui_overlay_services.h" #include "legacy_ui_overlay_services.h"
#include "app_core/document_canvas.h" #include "app_core/document_canvas.h"
@@ -190,40 +191,14 @@ void Canvas::import_equirectangular_thread(std::string file_path, std::shared_pt
void Canvas::export_equirectangular(std::string file_path, std::function<void()> on_complete) void Canvas::export_equirectangular(std::string file_path, std::function<void()> on_complete)
{ {
if (App::I->check_license()) pp::panopainter::execute_legacy_document_export_equirectangular(
{ std::move(file_path),
App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable { std::move(on_complete));
BT_SetTerminate();
export_equirectangular_thread(file_path);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
});
}
} }
void Canvas::export_equirectangular_thread(std::string file_path) void Canvas::export_equirectangular_thread(std::string file_path)
{ {
Image data; pp::panopainter::execute_legacy_document_export_equirectangular_thread(std::move(file_path));
App::I->render_task([&]
{
draw_merge(false);
Texture2D equirect = m_layers_merge.gen_equirect();
data = equirect.get_image();
});
LOG("writing %s", file_path.c_str());
if (file_path.substr(file_path.size() - 4) == ".jpg")
{
data.save_jpg(file_path, 100);
inject_xmp(file_path);
}
else if (file_path.substr(file_path.size() - 4) == ".png")
{
data.save_png(file_path);
}
App::I->publish_exported_image(file_path);
} }
void Canvas::inject_xmp(std::string jpg_path) void Canvas::inject_xmp(std::string jpg_path)

View File

@@ -3,6 +3,7 @@
#include "legacy_document_export_services.h" #include "legacy_document_export_services.h"
#include "app.h" #include "app.h"
#include "canvas.h"
#include "legacy_document_canvas_services.h" #include "legacy_document_canvas_services.h"
#include "paint_renderer/compositor.h" #include "paint_renderer/compositor.h"
@@ -130,6 +131,27 @@ pp::foundation::Status write_export_binary_file(std::string_view path, std::span
return pp::foundation::Status::success(); return pp::foundation::Status::success();
} }
void execute_legacy_document_export_equirectangular_thread_impl(std::string file_path)
{
Image data;
App::I->render_task([&] {
Canvas::I->draw_merge(false);
Texture2D equirect = Canvas::I->m_layers_merge.gen_equirect();
data = equirect.get_image();
});
LOG("writing %s", file_path.c_str());
if (pp::app::document_export_path_is_jpeg_target(file_path)) {
data.save_jpg(file_path, 100);
Canvas::I->inject_xmp(file_path);
} else if (pp::app::document_export_path_is_png_target(file_path)) {
data.save_png(file_path);
}
App::I->publish_exported_image(file_path);
}
class LegacyExportWriteServices final class LegacyExportWriteServices final
: public pp::app::DocumentCubeFaceExportWriteServices : public pp::app::DocumentCubeFaceExportWriteServices
, public pp::app::DocumentDepthExportWriteServices , public pp::app::DocumentDepthExportWriteServices
@@ -868,6 +890,28 @@ private:
} // namespace } // namespace
void execute_legacy_document_export_equirectangular(
std::string file_path,
std::function<void()> on_complete)
{
if (App::I->check_license()) {
App::I->runtime().canvas_async_task([file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
execute_legacy_document_export_equirectangular_thread_impl(file_path);
if (on_complete) {
App::I->ui_task([on_complete = std::move(on_complete)]() mutable {
on_complete();
});
}
});
}
}
void execute_legacy_document_export_equirectangular_thread(std::string file_path)
{
execute_legacy_document_export_equirectangular_thread_impl(std::move(file_path));
}
pp::foundation::Status execute_legacy_document_export_file( pp::foundation::Status execute_legacy_document_export_file(
App& app, App& app,
const pp::app::DocumentExportFileTarget& target) const pp::app::DocumentExportFileTarget& target)

View File

@@ -5,6 +5,9 @@
class App; class App;
#include <functional>
#include <string>
namespace pp::panopainter { namespace pp::panopainter {
[[nodiscard]] pp::foundation::Status execute_legacy_document_export_file( [[nodiscard]] pp::foundation::Status execute_legacy_document_export_file(
@@ -35,4 +38,10 @@ namespace pp::panopainter {
std::string_view path, std::string_view path,
bool asynchronous); bool asynchronous);
void execute_legacy_document_export_equirectangular(
std::string file_path,
std::function<void()> on_complete);
void execute_legacy_document_export_equirectangular_thread(std::string file_path);
} // namespace pp::panopainter } // namespace pp::panopainter

View File

@@ -38,6 +38,33 @@ void task_dispatch_does_not_wait_for_stopped_worker(pp::tests::Harness& harness)
PP_EXPECT(harness, !plan.wait_for_completion); PP_EXPECT(harness, !plan.wait_for_completion);
} }
void runtime_task_dispatch_rejects_stopped_worker_enqueue(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_runtime_task_dispatch(false, true, 2, false, true, true);
PP_EXPECT(harness, !plan.execute_immediately);
PP_EXPECT(harness, !plan.queue_task);
PP_EXPECT(harness, !plan.remove_matching_unique_task);
PP_EXPECT(harness, !plan.notify_worker);
PP_EXPECT(harness, !plan.wait_for_completion);
PP_EXPECT(harness, !plan.request_redraw);
PP_EXPECT(harness, !plan.reject_unsafe_cross_thread_dispatch);
PP_EXPECT(harness, plan.reject_stopped_worker_dispatch);
}
void runtime_task_dispatch_executes_immediately_on_target_thread(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_runtime_task_dispatch(true, true, 3, false, true, true);
PP_EXPECT(harness, plan.execute_immediately);
PP_EXPECT(harness, !plan.queue_task);
PP_EXPECT(harness, !plan.remove_matching_unique_task);
PP_EXPECT(harness, !plan.notify_worker);
PP_EXPECT(harness, !plan.wait_for_completion);
PP_EXPECT(harness, plan.request_redraw);
PP_EXPECT(harness, !plan.reject_stopped_worker_dispatch);
}
void task_dispatch_rejects_unsafe_cross_thread_mutations(pp::tests::Harness& harness) void task_dispatch_rejects_unsafe_cross_thread_mutations(pp::tests::Harness& harness)
{ {
const auto plan = pp::app::plan_app_task_dispatch(false, true, 2, true, true, false, true); const auto plan = pp::app::plan_app_task_dispatch(false, true, 2, true, true, false, true);
@@ -137,6 +164,8 @@ int main()
harness.run("task dispatch executes immediately on target thread", task_dispatch_executes_immediately_on_target_thread); harness.run("task dispatch executes immediately on target thread", task_dispatch_executes_immediately_on_target_thread);
harness.run("task dispatch queues unique work and waits for running worker", task_dispatch_queues_unique_work_and_waits_for_running_worker); harness.run("task dispatch queues unique work and waits for running worker", task_dispatch_queues_unique_work_and_waits_for_running_worker);
harness.run("task dispatch does not wait for stopped worker", task_dispatch_does_not_wait_for_stopped_worker); harness.run("task dispatch does not wait for stopped worker", task_dispatch_does_not_wait_for_stopped_worker);
harness.run("runtime task dispatch rejects stopped worker enqueue", runtime_task_dispatch_rejects_stopped_worker_enqueue);
harness.run("runtime task dispatch executes immediately on target thread", runtime_task_dispatch_executes_immediately_on_target_thread);
harness.run("task dispatch can reject unsafe cross-thread mutations", task_dispatch_rejects_unsafe_cross_thread_mutations); harness.run("task dispatch can reject unsafe cross-thread mutations", task_dispatch_rejects_unsafe_cross_thread_mutations);
harness.run("render queue drain wraps non empty work in context", render_queue_drain_wraps_non_empty_work_in_context); harness.run("render queue drain wraps non empty work in context", render_queue_drain_wraps_non_empty_work_in_context);
harness.run("ui thread tick runs tasks and schedules redraw", ui_thread_tick_runs_tasks_and_schedules_redraw); harness.run("ui thread tick runs tasks and schedules redraw", ui_thread_tick_runs_tasks_and_schedules_redraw);