#pragma once #include "prism_engineering/worker_pool.hpp" #include #include #include #include #include #include #include #include #include #include namespace prism_engineering { struct Rgb { std::uint8_t r{}, g{}, b{}; bool operator==(const Rgb&) const = default; }; struct PreparedImage { std::uint32_t width{}, height{}; std::vector pixels; }; // The descriptor is copied by value. No borrowed pixel input escapes the caller. struct ImageRequest { std::uint64_t generation{}; std::uint32_t width{8}, height{8}; std::uint8_t blue{64}; std::shared_ptr gate; // Optional deterministic test seam. bool inject_failure{}; }; struct Completion { std::uint64_t generation{}; Status status{Status::pending}; PreparedImage image; }; enum class Admission { closed, full, invalid_dimensions, invalid_generation }; struct PipelineOptions { std::size_t workers{2}; std::optional fail_launch_after{}; }; // One owning application thread calls the public methods. Workers only use the // private loop. Destruction/shutdown must not race with public calls. class AssetPipeline { public: static constexpr std::size_t capacity = 8; static constexpr std::size_t worker_limit = 4; static constexpr std::uint32_t extent_limit = 64; [[nodiscard]] static std::expected, Error> create(PipelineOptions options = {}); ~AssetPipeline(); AssetPipeline(const AssetPipeline&) = delete; AssetPipeline& operator=(const AssetPipeline&) = delete; // Brief mutex acquisition, but no wait for rendering or queue capacity. [[nodiscard]] std::expected try_submit(ImageRequest request); [[nodiscard]] std::optional try_take(); // CLI/test convenience ONLY. A UI uses try_take from its regular event loop. // While admission is open this waits for a completion, even if no job exists. // After request_stop(), call shutdown() before draining: shutdown finalizes // queued slots. request_stop() alone does not guarantee a terminal result. [[nodiscard]] std::optional wait_take(); void request_stop(); void shutdown(); // Stop admission, cancel, join, preserve terminal results. [[nodiscard]] bool runtime_failed() const noexcept { return failed_.load(); } private: AssetPipeline() = default; enum class Phase { empty, queued, running, complete }; struct Slot { Phase phase{Phase::empty}; ImageRequest request; Completion result; }; [[nodiscard]] std::optional take_locked(); [[nodiscard]] bool has_completed_locked() const; void run_worker(); std::mutex mutex_; std::condition_variable_any changed_; std::array slots_{}; std::stop_source stop_; std::atomic failed_{false}; std::uint64_t last_generation_{}; bool accepting_{true}; bool joined_{}; // Last member, and explicitly joined: every borrowed member above survives. std::array workers_{}; }; // Single-owner publication policy. Request identity and published identity differ. class Gallery { public: [[nodiscard]] std::expected request(AssetPipeline& pipeline, ImageRequest request); [[nodiscard]] bool apply(Completion completion); [[nodiscard]] std::uint64_t requested_generation() const noexcept { return requested_; } [[nodiscard]] std::uint64_t published_generation() const noexcept { return published_; } [[nodiscard]] Status latest_status() const noexcept { return latest_; } [[nodiscard]] const PreparedImage& image() const noexcept { return image_; } private: std::uint64_t requested_{}; std::uint64_t published_{}; bool settled_{}; Status latest_{Status::pending}; PreparedImage image_; }; } // namespace prism_engineering