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_session_services.cpp
src/legacy_document_session_services.h
${PP_PLATFORM_WEB_SOURCES}
src/version.cpp
)

View File

@@ -18,6 +18,23 @@ agent or engineer to remove them without reconstructing context from chat.
## 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.
`src/platform_windows/windows_runtime_flow.*` now owns the live Win32
startup/session composition flow, including the bound app/session preflight

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,17 @@ struct AppTaskDispatchPlan {
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 {
bool set_redraw = 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
{
return AppAsyncRedrawPlan {};

View File

@@ -3,6 +3,34 @@
#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()
: prepared_file_worker_([this](std::stop_token stop_token)
{
@@ -23,11 +51,13 @@ AppRuntime::~AppRuntime()
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_;
}
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_;
}
@@ -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)
{
for (;;)
@@ -177,14 +375,17 @@ void AppRuntime::canvas_async_worker_stop()
void AppRuntime::render_thread_tick(App& app)
{
static uint32_t count = 0;
{
std::lock_guard<std::mutex> lock(render_task_mutex_);
render_thread_id_ = std::this_thread::get_id();
}
std::deque<AppTask> working_list;
pp::app::AppQueueDrainPlan drain_plan;
{
std::unique_lock<std::mutex> lock(render_task_mutex_);
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)
return;
working_list = std::move(render_tasklist_);
@@ -196,7 +397,7 @@ void AppRuntime::render_thread_tick(App& app)
while (!working_list.empty())
{
count++;
working_list.front()();
execute_render_worker_task(working_list.front());
working_list.pop_front();
}
app.async_end();
@@ -207,39 +408,46 @@ void AppRuntime::render_thread_main(App& app, std::stop_token stop_token)
{
BT_SetTerminate();
{
std::lock_guard<std::mutex> lock(render_task_mutex_);
render_thread_id_ = std::this_thread::get_id();
render_running_ = pp::app::plan_app_thread_start().mark_running;
while (render_running_ && !stop_token.stop_requested())
}
render_running_.store(pp::app::plan_app_thread_start().mark_running, std::memory_order_release);
for (;;)
{
std::deque<AppTask> working_list;
pp::app::AppQueueDrainPlan drain_plan;
{
std::unique_lock<std::mutex> lock(render_task_mutex_);
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_);
}
if (drain_plan.wrap_in_render_context)
{
app.async_start();
while (!working_list.empty())
{
working_list.front()();
execute_render_worker_task(working_list.front());
working_list.pop_front();
}
app.async_end();
}
}
}
void AppRuntime::ui_thread_tick(App& app)
{
{
std::lock_guard<std::mutex> lock(ui_task_mutex_);
ui_thread_id_ = std::this_thread::get_id();
}
std::deque<AppTask> working_list;
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_);
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_);
}
@@ -255,7 +463,7 @@ void AppRuntime::ui_thread_tick(App& app)
{
while (!working_list.empty())
{
working_list.front()();
execute_ui_worker_task(working_list.front());
working_list.pop_front();
}
}
@@ -282,8 +490,11 @@ void AppRuntime::ui_thread_main(App& app, std::stop_token stop_token)
{
BT_SetTerminate();
{
std::lock_guard<std::mutex> lock(ui_task_mutex_);
ui_thread_id_ = std::this_thread::get_id();
ui_running_ = pp::app::plan_app_thread_start().mark_running;
}
ui_running_.store(pp::app::plan_app_thread_start().mark_running, std::memory_order_release);
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_reloader = 0;
int rendered_frames = 0;
while (ui_running_ && !stop_token.stop_requested())
for (;;)
{
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_);
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();
});
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())
{
while (!working_list.empty())
{
working_list.front()();
execute_ui_worker_task(working_list.front());
working_list.pop_front();
}
}
@@ -383,14 +602,14 @@ void AppRuntime::render_thread_start(App& app)
{
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()
{
const auto plan = pp::app::plan_app_thread_stop(render_thread_.joinable());
if (plan.mark_not_running)
render_running_ = false;
render_running_.store(false, std::memory_order_release);
if (plan.join_thread)
render_thread_.request_stop();
if (plan.notify_worker)
@@ -407,14 +626,14 @@ void AppRuntime::ui_thread_start(App& app)
{
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()
{
const auto plan = pp::app::plan_app_thread_stop(ui_thread_.joinable());
if (plan.mark_not_running)
ui_running_ = false;
ui_running_.store(false, std::memory_order_release);
if (plan.join_thread)
ui_thread_.request_stop();
if (plan.notify_worker)

View File

@@ -2,6 +2,7 @@
#include "app_core/app_thread.h"
#include <atomic>
#include <algorithm>
#include <condition_variable>
#include <cstddef>
@@ -39,8 +40,8 @@ public:
[[nodiscard]] bool is_render_thread() const noexcept;
[[nodiscard]] bool is_ui_thread() const noexcept;
[[nodiscard]] bool request_redraw() const noexcept { return request_redraw_; }
void clear_request_redraw() noexcept { request_redraw_ = false; }
[[nodiscard]] bool request_redraw() const noexcept { return request_redraw_.load(std::memory_order_acquire); }
void clear_request_redraw() noexcept { request_redraw_.store(false, std::memory_order_release); }
void notify_render_worker() noexcept;
void notify_ui_worker() noexcept;
@@ -49,6 +50,11 @@ public:
void main_thread_task(std::packaged_task<void()> task);
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_main(App& app, std::stop_token stop_token);
void render_thread_start(App& app);
@@ -62,68 +68,13 @@ public:
template<typename T>
std::future<void> render_task_async(T task, bool unique = false)
{
AppTask pt(task);
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;
return render_task_async(AppTask(std::move(task)), unique);
}
template<typename T>
void render_task(T task)
{
AppTask pt(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();
render_task(AppTask(std::move(task)));
}
void render_sync()
@@ -134,70 +85,13 @@ public:
template<typename T>
std::future<void> ui_task_async(T task, bool unique = false)
{
AppTask pt(task);
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;
return ui_task_async(AppTask(std::move(task)), unique);
}
template<typename T>
void ui_task(T task)
{
AppTask pt(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;
ui_task(AppTask(std::move(task)));
}
void ui_sync()
@@ -227,17 +121,17 @@ private:
std::mutex main_thread_task_mutex_;
std::deque<AppTask> render_tasklist_;
std::mutex render_task_mutex_;
mutable std::mutex render_task_mutex_;
std::condition_variable render_cv_;
std::jthread render_thread_;
std::thread::id render_thread_id_;
bool render_running_ = false;
std::atomic_bool render_running_ = false;
std::deque<AppTask> ui_tasklist_;
std::mutex ui_task_mutex_;
mutable std::mutex ui_task_mutex_;
std::condition_variable ui_cv_;
std::jthread ui_thread_;
std::thread::id ui_thread_id_;
bool ui_running_ = false;
bool request_redraw_ = false;
std::atomic_bool ui_running_ = false;
std::atomic_bool request_redraw_ = false;
};

View File

@@ -230,7 +230,7 @@ void App::async_redraw()
if (plan.set_redraw)
redraw = true;
if (plan.notify_ui)
runtime_.notify_ui_worker();
runtime().notify_ui_worker();
}
void App::async_end()
@@ -370,5 +370,5 @@ void App::rec_loop()
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 "app.h"
#include "legacy_canvas_draw_merge_services.h"
#include "legacy_document_export_services.h"
#include "legacy_ui_gl_dispatch.h"
#include "legacy_ui_overlay_services.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)
{
if (App::I->check_license())
{
App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
export_equirectangular_thread(file_path);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
});
}
pp::panopainter::execute_legacy_document_export_equirectangular(
std::move(file_path),
std::move(on_complete));
}
void Canvas::export_equirectangular_thread(std::string file_path)
{
Image data;
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);
pp::panopainter::execute_legacy_document_export_equirectangular_thread(std::move(file_path));
}
void Canvas::inject_xmp(std::string jpg_path)

View File

@@ -3,6 +3,7 @@
#include "legacy_document_export_services.h"
#include "app.h"
#include "canvas.h"
#include "legacy_document_canvas_services.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();
}
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
: public pp::app::DocumentCubeFaceExportWriteServices
, public pp::app::DocumentDepthExportWriteServices
@@ -868,6 +890,28 @@ private:
} // 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(
App& app,
const pp::app::DocumentExportFileTarget& target)

View File

@@ -5,6 +5,9 @@
class App;
#include <functional>
#include <string>
namespace pp::panopainter {
[[nodiscard]] pp::foundation::Status execute_legacy_document_export_file(
@@ -35,4 +38,10 @@ namespace pp::panopainter {
std::string_view path,
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

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);
}
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)
{
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 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("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("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);