diff --git a/CMakeLists.txt b/CMakeLists.txt index 60990bd..00a65a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ add_library(pp_foundation STATIC src/foundation/binary_stream.cpp src/foundation/log.cpp src/foundation/parse.cpp + src/foundation/task_queue.cpp src/foundation/trace.cpp) target_include_directories(pp_foundation PUBLIC diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 849b520..da6cf80 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -80,8 +80,8 @@ Known local toolchain state: `platform-build` automation wrapper for `pp_foundation`, `pp_assets`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, `pp_ui_core`, `pano_cli`, and their current headless test binaries, - including foundation logging, PPI header, paint stroke sampling, and layout - XML parse coverage. + including foundation logging/task queue coverage, PPI header, paint stroke + sampling, and layout XML parse coverage. - `panopainter_validate_shaders` validates the current combined GLSL shader files for one vertex stage marker, one fragment stage marker, valid marker order, and existing relative includes. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index b467d3e..1fd3237 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -304,9 +304,9 @@ Goal: split libraries while keeping current app behavior. Status: started. `pp_foundation` exists with binary stream utilities and boundary/overread tests. It also owns strict decimal `uint32` parsing used by `pano_cli`, with rejection tests for empty, signed, mixed, and overflowing -input. A structured logging facade and deterministic `TraceRecorder` now -record component/name/thread/frame/stroke metadata with filtering and invalid-end -tests. `pp_assets` has started with +input. A structured logging facade, bounded FIFO task queue, and deterministic +`TraceRecorder` now record component/name/thread/frame/stroke metadata with +filtering, capacity, and invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection plus PPI header recognition, with corrupt/truncated/unsupported tests. `pp_paint` has started with CPU reference math for the five current shader @@ -524,7 +524,7 @@ Last verified on 2026-06-01: ```powershell cmake --preset windows-msvc-default -cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter +cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter ctest --preset desktop-fast --build-config Debug powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli @@ -540,6 +540,7 @@ Results: - `pp_foundation_binary_stream_tests` passed. - `pp_foundation_log_tests` passed. - `pp_foundation_parse_tests` passed. +- `pp_foundation_task_queue_tests` passed. - `pp_foundation_trace_tests` passed. - `pp_assets_image_format_tests` passed. - `pp_assets_ppi_header_tests` passed. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index f3239d1..9ce5a2f 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param( [string[]]$Presets = @("android-arm64"), - [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests") + [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests") ) $ErrorActionPreference = "Stop" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index 1b219e7..68d8c0c 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u preset="${1:-android-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}" start="$(date +%s)" cmake --preset "$preset" diff --git a/src/foundation/task_queue.cpp b/src/foundation/task_queue.cpp new file mode 100644 index 0000000..eac7c3d --- /dev/null +++ b/src/foundation/task_queue.cpp @@ -0,0 +1,83 @@ +#include "foundation/task_queue.h" + +namespace pp::foundation { + +TaskQueue::TaskQueue(std::size_t max_entries) noexcept + : max_entries_(max_entries) +{ +} + +std::size_t TaskQueue::size() const noexcept +{ + return tasks_.size(); +} + +bool TaskQueue::empty() const noexcept +{ + return tasks_.empty(); +} + +std::size_t TaskQueue::max_entries() const noexcept +{ + return max_entries_; +} + +Status TaskQueue::push(TaskItem task) +{ + if (max_entries_ == 0U || max_entries_ > max_task_queue_entries) { + return Status::out_of_range("task queue capacity is outside the configured range"); + } + + if (task.callback == nullptr) { + return Status::invalid_argument("task callback must not be null"); + } + + if (tasks_.size() >= max_entries_) { + return Status::out_of_range("task queue is full"); + } + + tasks_.push_back(task); + return Status::success(); +} + +Result TaskQueue::pop() noexcept +{ + if (tasks_.empty()) { + return Result::failure(Status::out_of_range("task queue is empty")); + } + + const auto task = tasks_.front(); + tasks_.pop_front(); + return Result::success(task); +} + +Status TaskQueue::run_next() noexcept +{ + auto task = pop(); + if (!task.ok()) { + return task.status(); + } + + task.value().callback(task.value().user_data); + return Status::success(); +} + +std::size_t TaskQueue::run_all() noexcept +{ + std::size_t count = 0; + while (!tasks_.empty()) { + const auto status = run_next(); + if (!status.ok()) { + break; + } + ++count; + } + return count; +} + +void TaskQueue::clear() noexcept +{ + tasks_.clear(); +} + +} diff --git a/src/foundation/task_queue.h b/src/foundation/task_queue.h new file mode 100644 index 0000000..2d24464 --- /dev/null +++ b/src/foundation/task_queue.h @@ -0,0 +1,40 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include +#include + +namespace pp::foundation { + +constexpr std::size_t max_task_queue_entries = 65536; + +using TaskCallback = void (*)(void* user_data) noexcept; + +struct TaskItem { + TaskCallback callback = nullptr; + void* user_data = nullptr; + std::uint64_t id = 0; +}; + +class TaskQueue { +public: + explicit TaskQueue(std::size_t max_entries = max_task_queue_entries) noexcept; + + [[nodiscard]] std::size_t size() const noexcept; + [[nodiscard]] bool empty() const noexcept; + [[nodiscard]] std::size_t max_entries() const noexcept; + + [[nodiscard]] Status push(TaskItem task); + [[nodiscard]] Result pop() noexcept; + [[nodiscard]] Status run_next() noexcept; + [[nodiscard]] std::size_t run_all() noexcept; + void clear() noexcept; + +private: + std::size_t max_entries_ = max_task_queue_entries; + std::deque tasks_; +}; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9ae3545..e2a61b2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -36,6 +36,16 @@ add_test(NAME pp_foundation_parse_tests COMMAND pp_foundation_parse_tests) set_tests_properties(pp_foundation_parse_tests PROPERTIES LABELS "foundation;desktop-fast") +add_executable(pp_foundation_task_queue_tests + foundation/task_queue_tests.cpp) +target_link_libraries(pp_foundation_task_queue_tests PRIVATE + pp_foundation + pp_test_harness) + +add_test(NAME pp_foundation_task_queue_tests COMMAND pp_foundation_task_queue_tests) +set_tests_properties(pp_foundation_task_queue_tests PROPERTIES + LABELS "foundation;desktop-fast") + add_executable(pp_foundation_trace_tests foundation/trace_tests.cpp) target_link_libraries(pp_foundation_trace_tests PRIVATE diff --git a/tests/foundation/task_queue_tests.cpp b/tests/foundation/task_queue_tests.cpp new file mode 100644 index 0000000..6a4d198 --- /dev/null +++ b/tests/foundation/task_queue_tests.cpp @@ -0,0 +1,109 @@ +#include "foundation/task_queue.h" +#include "test_harness.h" + +using pp::foundation::StatusCode; +using pp::foundation::TaskItem; +using pp::foundation::TaskQueue; +using pp::foundation::max_task_queue_entries; + +namespace { + +struct Counter { + int value = 0; +}; + +void increment(void* user_data) noexcept +{ + auto* counter = static_cast(user_data); + ++counter->value; +} + +void add_two(void* user_data) noexcept +{ + auto* counter = static_cast(user_data); + counter->value += 2; +} + +void runs_tasks_in_fifo_order(pp::tests::Harness& h) +{ + Counter counter; + TaskQueue queue(4); + + PP_EXPECT(h, queue.push(TaskItem { .callback = increment, .user_data = &counter, .id = 1 }).ok()); + PP_EXPECT(h, queue.push(TaskItem { .callback = add_two, .user_data = &counter, .id = 2 }).ok()); + PP_EXPECT(h, queue.size() == 2U); + PP_EXPECT(h, queue.run_next().ok()); + PP_EXPECT(h, counter.value == 1); + PP_EXPECT(h, queue.run_next().ok()); + PP_EXPECT(h, counter.value == 3); + PP_EXPECT(h, queue.empty()); +} + +void pops_without_running(pp::tests::Harness& h) +{ + Counter counter; + TaskQueue queue(2); + + PP_EXPECT(h, queue.push(TaskItem { .callback = increment, .user_data = &counter, .id = 42 }).ok()); + const auto task = queue.pop(); + + PP_EXPECT(h, task.ok()); + PP_EXPECT(h, task.value().id == 42U); + PP_EXPECT(h, counter.value == 0); + PP_EXPECT(h, queue.empty()); +} + +void rejects_invalid_or_excessive_work(pp::tests::Harness& h) +{ + Counter counter; + TaskQueue queue(1); + TaskQueue invalid_queue(0); + TaskQueue too_large(max_task_queue_entries + 1U); + + const auto null_task = queue.push(TaskItem {}); + PP_EXPECT(h, !null_task.ok()); + PP_EXPECT(h, null_task.code == StatusCode::invalid_argument); + + PP_EXPECT(h, queue.push(TaskItem { .callback = increment, .user_data = &counter }).ok()); + const auto full = queue.push(TaskItem { .callback = increment, .user_data = &counter }); + PP_EXPECT(h, !full.ok()); + PP_EXPECT(h, full.code == StatusCode::out_of_range); + + const auto invalid_capacity = invalid_queue.push(TaskItem { .callback = increment, .user_data = &counter }); + const auto excessive_capacity = too_large.push(TaskItem { .callback = increment, .user_data = &counter }); + PP_EXPECT(h, !invalid_capacity.ok()); + PP_EXPECT(h, invalid_capacity.code == StatusCode::out_of_range); + PP_EXPECT(h, !excessive_capacity.ok()); + PP_EXPECT(h, excessive_capacity.code == StatusCode::out_of_range); +} + +void run_all_and_clear_are_bounded(pp::tests::Harness& h) +{ + Counter counter; + TaskQueue queue(4); + + PP_EXPECT(h, queue.max_entries() == 4U); + PP_EXPECT(h, queue.push(TaskItem { .callback = increment, .user_data = &counter }).ok()); + PP_EXPECT(h, queue.push(TaskItem { .callback = increment, .user_data = &counter }).ok()); + PP_EXPECT(h, queue.run_all() == 2U); + PP_EXPECT(h, counter.value == 2); + + PP_EXPECT(h, queue.push(TaskItem { .callback = add_two, .user_data = &counter }).ok()); + queue.clear(); + PP_EXPECT(h, queue.empty()); + const auto empty_pop = queue.pop(); + PP_EXPECT(h, !empty_pop.ok()); + PP_EXPECT(h, empty_pop.status().code == StatusCode::out_of_range); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("runs_tasks_in_fifo_order", runs_tasks_in_fifo_order); + harness.run("pops_without_running", pops_without_running); + harness.run("rejects_invalid_or_excessive_work", rejects_invalid_or_excessive_work); + harness.run("run_all_and_clear_are_bounded", run_all_and_clear_are_bounded); + return harness.finish(); +}