Move prepared-file work into app runtime

This commit is contained in:
2026-06-16 08:24:19 +02:00
parent 640ebc4be4
commit 3e4eb89499
9 changed files with 162 additions and 110 deletions

View File

@@ -18,6 +18,22 @@ agent or engineer to remove them without reconstructing context from chat.
## Reductions ## Reductions
- 2026-06-16: `DEBT-0003` was narrowed again. Prepared-file background work
now runs through an `AppRuntime`-owned queue/worker in
`src/app_runtime.h/.cpp`, and `src/app_events.cpp` no longer defines a
retained static prepared-file worker; broader app task ownership and canvas
async worker ownership remain.
- 2026-06-16: `DEBT-0037` was narrowed again. `App::rec_loop()` in
`src/app.cpp` now routes its worker-iteration pointer lookup plus
`plan_recording_worker_iteration(...)` setup through a local helper instead
of carrying that setup inline; retained recording loop control, readback call
sites, and MP4 execution remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` current-mode draw
callback setup now routes through
`make_legacy_canvas_draw_merge_current_modes_draw(...)` in
`src/legacy_canvas_draw_merge_services.h` instead of keeping that callback
loop inline in `NodeCanvas::draw()`; broader canvas draw orchestration and
retained GL resource ownership remain.
- 2026-06-16: `DEBT-0037` was narrowed again. `App::rec_loop()` in - 2026-06-16: `DEBT-0037` was narrowed again. `App::rec_loop()` in
`src/app.cpp` now routes its coherent frame encode/update chunk through a `src/app.cpp` now routes its coherent frame encode/update chunk through a
local helper instead of carrying dirty-stroke clearing, equirect PBO local helper instead of carrying dirty-stroke clearing, equirect PBO

View File

@@ -117,8 +117,8 @@ Current architecture mismatches that must be treated as real blockers:
checkerboard background setup now route through retained draw-merge helpers, checkerboard background setup now route through retained draw-merge helpers,
with the cache-to-screen checkerboard-plane callback setup also reduced and with the cache-to-screen checkerboard-plane callback setup also reduced and
the merged-path per-plane merged-texture draw plus the smoothing-mask face the merged-path per-plane merged-texture draw plus the smoothing-mask face
shader/draw pass plus heightmap callback setup now routed through the same shader/draw pass plus heightmap and current-mode callback setup now routed
retained helper family. through the same retained helper family.
- `app_layout.cpp` and `app_dialogs.cpp` are still mixed shell/controller files - `app_layout.cpp` and `app_dialogs.cpp` are still mixed shell/controller files
rather than thin composition/binding surfaces. rather than thin composition/binding surfaces.
- `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use - `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use
@@ -134,10 +134,11 @@ Current architecture mismatches that must be treated as real blockers:
coordination flags now use `std::atomic` instead of unsynchronized globals, coordination flags now use `std::atomic` instead of unsynchronized globals,
while the main Win32 entrypoint now groups window/GL/task/VR state behind a while the main Win32 entrypoint now groups window/GL/task/VR state behind a
retained local state object instead of separate process-wide globals, the retained local state object instead of separate process-wide globals, the
prepared-file and canvas async workers now sit behind named retained local canvas async worker now sits behind a named retained local worker-state
worker-state helpers instead of bare static accessors, and `App::rec_loop()` helper instead of a bare static accessor, the prepared-file worker now lives
has a smaller local encode/update shell even though the retained recording under `AppRuntime` instead of a retained static app-events worker, and
loop still owns the worker-side readback flow. `App::rec_loop()` has a smaller local iteration/encode shell even though the
retained recording loop still owns the worker-side readback flow.
- Modern C++23 usage exists in extracted components, especially `std::span`, - Modern C++23 usage exists in extracted components, especially `std::span`,
explicit result/status objects, and a few concepts, but the live app still explicit result/status objects, and a few concepts, but the live app still
does not consistently express ownership, thread affinity, or renderer does not consistently express ownership, thread affinity, or renderer

View File

@@ -146,6 +146,9 @@ Current slice:
- `NodeCanvas` heightmap draw callback setup now also routes through - `NodeCanvas` heightmap draw callback setup now also routes through
`make_legacy_canvas_draw_merge_heightmap_draw(...)`, but the node still owns `make_legacy_canvas_draw_merge_heightmap_draw(...)`, but the node still owns
current-mode traversal and broader post-draw orchestration. current-mode traversal and broader post-draw orchestration.
- `NodeCanvas` current-mode draw callback setup now also routes through
`make_legacy_canvas_draw_merge_current_modes_draw(...)`, but grid-mode
traversal and broader post-draw orchestration are still inline.
- `NodeCanvas` smoothing-mask face shader setup plus per-face draw execution - `NodeCanvas` smoothing-mask face shader setup plus per-face draw execution
now also route through now also route through
`execute_legacy_canvas_draw_merge_smask_faces(...)`, but the node still owns `execute_legacy_canvas_draw_merge_smask_faces(...)`, but the node still owns
@@ -347,6 +350,8 @@ Current slice:
- `main.cpp` Win32 window handles, GL task/mutex state, splash-dialog state, - `main.cpp` Win32 window handles, GL task/mutex state, splash-dialog state,
stylus timers, and VR worker state now sit behind one retained local state stylus timers, and VR worker state now sit behind one retained local state
object instead of separate file-scope globals object instead of separate file-scope globals
- prepared-file background work now runs through an `AppRuntime`-owned worker
queue instead of a retained static worker in `src/app_events.cpp`
- retained `App` composition, task call sites, and platform/runtime entrypoint - retained `App` composition, task call sites, and platform/runtime entrypoint
coupling are still not fully reduced behind the runtime contract coupling are still not fully reduced behind the runtime contract
@@ -404,12 +409,16 @@ Current slice:
- `src/app_events.cpp` prepared-file worker ownership and `src/canvas.cpp` - `src/app_events.cpp` prepared-file worker ownership and `src/canvas.cpp`
async import/export/save/open worker ownership now also sit behind named async import/export/save/open worker ownership now also sit behind named
retained local worker-state helpers instead of bare static worker accessors retained local worker-state helpers instead of bare static worker accessors
- the prepared-file worker has now moved again into `AppRuntime`, removing the
retained static worker from `src/app_events.cpp`; the broader canvas async
worker still remains local because that slice is wider
- preview background rendering, recording, and the retained - preview background rendering, recording, and the retained
`NodePanelGrid::bake_uvs()` worker now also use `std::jthread`, but their `NodePanelGrid::bake_uvs()` worker now also use `std::jthread`, but their
retained loop/control flow is still open retained loop/control flow is still open
- `App::rec_loop()` now routes its frame encode/update chunk through a local - `App::rec_loop()` now routes its frame encode/update chunk through a local
helper, and `App::update()` no longer carries the dead update mutex residue, helper, its iteration-context setup now also routes through a local helper,
but retained recording loop control and readback ownership are still open and `App::update()` no longer carries the dead update mutex residue, but
retained recording loop control and readback ownership are still open
Write scope: Write scope:
- `src/canvas.cpp` - `src/canvas.cpp`

View File

@@ -643,6 +643,29 @@ void App::rec_export(std::string path)
namespace namespace
{ {
template <typename CanvasDocument>
struct RecordingWorkerIterationContext
{
Canvas* legacy_canvas = nullptr;
CanvasDocument* canvas_document = nullptr;
CanvasEncoder* encoder = nullptr;
pp::app::RecordingWorkerIterationPlan plan{};
};
template <typename CanvasDocument>
RecordingWorkerIterationContext<CanvasDocument> make_recording_worker_iteration_context(App& app)
{
RecordingWorkerIterationContext<CanvasDocument> context;
context.legacy_canvas = Canvas::I;
context.canvas_document = app.canvas ? app.canvas->m_canvas.get() : nullptr;
context.encoder = context.legacy_canvas ? context.legacy_canvas->m_encoder.get() : nullptr;
context.plan = pp::app::plan_recording_worker_iteration(
app.rec_running,
context.encoder != nullptr,
context.legacy_canvas != nullptr && context.canvas_document != nullptr);
return context;
}
template <typename CanvasDocument> template <typename CanvasDocument>
void encode_recording_frame( void encode_recording_frame(
App& app, App& app,
@@ -676,18 +699,12 @@ void App::rec_loop()
{ {
std::unique_lock<std::mutex> lock(rec_mutex); std::unique_lock<std::mutex> lock(rec_mutex);
rec_cv.wait(lock/*, [this] { return !(rec_frames.empty() && rec_running); }*/); rec_cv.wait(lock/*, [this] { return !(rec_frames.empty() && rec_running); }*/);
auto* legacy_canvas = Canvas::I; const auto iteration = make_recording_worker_iteration_context<CanvasDocument>(*this);
auto* canvas_document = canvas ? canvas->m_canvas.get() : nullptr; if (!iteration.plan.continue_running)
auto* encoder = legacy_canvas ? legacy_canvas->m_encoder.get() : nullptr;
const auto plan = pp::app::plan_recording_worker_iteration(
rec_running,
encoder != nullptr,
legacy_canvas != nullptr && canvas_document != nullptr);
if (!plan.continue_running)
break; break;
if (plan.encode_frame && legacy_canvas && canvas_document && encoder) if (iteration.plan.encode_frame && iteration.legacy_canvas && iteration.canvas_document && iteration.encoder)
encode_recording_frame(*this, plan, legacy_canvas, canvas_document, encoder); encode_recording_frame(*this, iteration.plan, iteration.legacy_canvas, iteration.canvas_document, iteration.encoder);
} }
} }

View File

@@ -6,12 +6,7 @@
#include "app_core/document_sharing.h" #include "app_core/document_sharing.h"
#include "platform_api/platform_services.h" #include "platform_api/platform_services.h"
#include <condition_variable>
#include <deque>
#include <functional> #include <functional>
#include <mutex>
#include <stop_token>
#include <thread>
#ifdef __LINUX__ #ifdef __LINUX__
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
@@ -22,88 +17,6 @@
namespace { namespace {
class LegacyPreparedFileWorker final {
public:
LegacyPreparedFileWorker()
: worker_([this](std::stop_token stop_token) {
run(stop_token);
})
{
}
~LegacyPreparedFileWorker()
{
shutdown();
}
void post(std::function<void()> task)
{
{
std::lock_guard<std::mutex> lock(mutex_);
if (stopping_)
return;
tasks_.push_back(std::move(task));
}
cv_.notify_one();
}
private:
void shutdown()
{
{
std::lock_guard<std::mutex> lock(mutex_);
stopping_ = true;
}
cv_.notify_all();
}
void run(std::stop_token stop_token)
{
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [&] {
return stopping_ || stop_token.stop_requested() || !tasks_.empty();
});
if ((stopping_ || stop_token.stop_requested()) && tasks_.empty())
break;
task = std::move(tasks_.front());
tasks_.pop_front();
}
if (task) {
try {
task();
} catch (...) {
LOG("prepared file worker task failed");
}
}
}
}
std::mutex mutex_;
std::condition_variable cv_;
std::deque<std::function<void()>> tasks_;
bool stopping_ = false;
std::jthread worker_;
};
struct RetainedPreparedFileWorkerState final {
void post(std::function<void()> task)
{
worker.post(std::move(task));
}
LegacyPreparedFileWorker worker;
};
RetainedPreparedFileWorkerState& retained_prepared_file_worker_state()
{
static RetainedPreparedFileWorkerState state;
return state;
}
[[nodiscard]] GLint rgba8_internal_format() noexcept [[nodiscard]] GLint rgba8_internal_format() noexcept
{ {
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format()); return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
@@ -293,7 +206,7 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam
LOG("App::pick_file_save %s", target.path.c_str()); LOG("App::pick_file_save %s", target.path.c_str());
if (target.write_on_background_thread) { if (target.write_on_background_thread) {
auto* app = this; auto* app = this;
retained_prepared_file_worker_state().post([ runtime_.prepared_file_task([
app, app,
writer = std::move(writer), writer = std::move(writer),
callback = std::move(callback), callback = std::move(callback),

View File

@@ -3,6 +3,19 @@
#include "app.h" #include "app.h"
AppRuntime::AppRuntime()
: prepared_file_worker_([this](std::stop_token stop_token)
{
prepared_file_worker_main(stop_token);
})
{
}
AppRuntime::~AppRuntime()
{
prepared_file_worker_stop();
}
bool AppRuntime::is_render_thread() const noexcept bool AppRuntime::is_render_thread() const noexcept
{ {
return std::this_thread::get_id() == render_thread_id_; return std::this_thread::get_id() == render_thread_id_;
@@ -23,6 +36,62 @@ void AppRuntime::notify_ui_worker() noexcept
ui_cv_.notify_all(); ui_cv_.notify_all();
} }
void AppRuntime::prepared_file_task(std::function<void()> task)
{
{
std::lock_guard<std::mutex> lock(prepared_file_task_mutex_);
if (!prepared_file_running_)
return;
prepared_file_tasklist_.push_back(std::move(task));
}
prepared_file_cv_.notify_one();
}
void AppRuntime::prepared_file_worker_main(std::stop_token stop_token)
{
for (;;)
{
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(prepared_file_task_mutex_);
prepared_file_cv_.wait(lock, [this, &stop_token]
{
return stop_token.stop_requested() || !prepared_file_running_ || !prepared_file_tasklist_.empty();
});
if ((stop_token.stop_requested() || !prepared_file_running_) && prepared_file_tasklist_.empty())
break;
task = std::move(prepared_file_tasklist_.front());
prepared_file_tasklist_.pop_front();
}
if (task)
{
try
{
task();
}
catch (...)
{
LOG("prepared file worker task failed");
}
}
}
}
void AppRuntime::prepared_file_worker_stop()
{
{
std::lock_guard<std::mutex> lock(prepared_file_task_mutex_);
prepared_file_running_ = false;
}
prepared_file_cv_.notify_all();
if (prepared_file_worker_.joinable())
{
prepared_file_worker_.request_stop();
prepared_file_worker_.join();
}
}
void AppRuntime::render_thread_tick(App& app) void AppRuntime::render_thread_tick(App& app)
{ {
static uint32_t count = 0; static uint32_t count = 0;

View File

@@ -34,6 +34,9 @@ struct AppTask : public std::packaged_task<void()>
class AppRuntime class AppRuntime
{ {
public: public:
AppRuntime();
~AppRuntime();
[[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_; }
@@ -41,6 +44,7 @@ public:
void notify_render_worker() noexcept; void notify_render_worker() noexcept;
void notify_ui_worker() noexcept; void notify_ui_worker() noexcept;
void prepared_file_task(std::function<void()> 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);
@@ -199,6 +203,15 @@ public:
} }
private: private:
void prepared_file_worker_main(std::stop_token stop_token);
void prepared_file_worker_stop();
std::deque<std::function<void()>> prepared_file_tasklist_;
std::mutex prepared_file_task_mutex_;
std::condition_variable prepared_file_cv_;
std::jthread prepared_file_worker_;
bool prepared_file_running_ = true;
std::deque<AppTask> render_tasklist_; std::deque<AppTask> render_tasklist_;
std::mutex render_task_mutex_; std::mutex render_task_mutex_;
std::condition_variable render_cv_; std::condition_variable render_cv_;

View File

@@ -250,6 +250,19 @@ template <typename GridT>
}; };
} }
template <typename ModesT>
[[nodiscard]] inline auto make_legacy_canvas_draw_merge_current_modes_draw(
ModesT* modes,
const glm::mat4& ortho_proj,
const glm::mat4& proj,
const glm::mat4& camera)
{
return [modes, ortho_proj, proj, camera] {
for (auto& mode : *modes)
mode->on_Draw(ortho_proj, proj, camera);
};
}
struct LegacyCanvasDrawMergeSmaskFacesExecution { struct LegacyCanvasDrawMergeSmaskFacesExecution {
std::function<void()> set_active_texture_unit; std::function<void()> set_active_texture_unit;
std::function<void()> enable_blend; std::function<void()> enable_blend;

View File

@@ -770,10 +770,11 @@ void NodeCanvas::draw()
mode->on_Draw(ortho_proj, proj, camera); mode->on_Draw(ortho_proj, proj, camera);
}, },
.draw_heightmap = pp::panopainter::make_legacy_canvas_draw_merge_heightmap_draw(App::I->grid, proj, camera), .draw_heightmap = pp::panopainter::make_legacy_canvas_draw_merge_heightmap_draw(App::I->grid, proj, camera),
.draw_current_modes = [&] { .draw_current_modes = pp::panopainter::make_legacy_canvas_draw_merge_current_modes_draw(
for (auto& mode : *m_canvas->m_mode) m_canvas->m_mode,
mode->on_Draw(ortho_proj, proj, camera); ortho_proj,
}, proj,
camera),
}); });
if (m_density != 1.f) if (m_density != 1.f)