MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
WindowContainer.hpp
Go to the documentation of this file.
1#pragma once
2
5
6namespace MayaFlux::Core {
7class Window;
8class VKImage;
9}
10
11namespace MayaFlux::Buffers {
12class VKBuffer;
13}
14
15namespace MayaFlux::Kakshya {
16
17/**
18 * @class WindowContainer
19 * @brief SignalSourceContainer wrapping a live GLFW/Vulkan window surface.
20 *
21 * Exposes a window's rendered surface as addressable N-dimensional data.
22 * Dimensions follow IMAGE_COLOR convention:
23 * dims[0] → SPATIAL_Y (height)
24 * dims[1] → SPATIAL_X (width)
25 * dims[2] → CHANNEL
26 *
27 * Region semantics:
28 * Regions are registered through the inherited RegionGroup API
29 * (add_region_group / get_all_region_groups / remove_region_group).
30 * load_region() and unload_region() are no-ops on this container — all
31 * surface data is always available after a GPU readback; region selection
32 * is a processor concern, not a container concern.
33 *
34 * Processing model:
35 * - The default processor (WindowAccessProcessor) performs one full-surface
36 * GPU readback per process() call into processed_data[0].
37 * - The processing chain (SpatialRegionProcessor) extracts registered
38 * regions from the readback as separate DataVariant entries.
39 * - get_region_data() crops directly from processed_data[0] if it is
40 * non-empty; returns empty if no readback has occurred yet.
41 * - The container never calls process() itself — callers drive the chain.
42 * GPU bridge:
43 * - to_image() uploads processed_data[0] to a new VKImage via TextureLoom.
44 * - to_image(staging) does the same reusing a caller-supplied staging buffer
45 * to avoid per-call VkBuffer allocation in per-frame paths.
46 * - region_to_image() performs a CPU-side crop then uploads the result.
47 * - get_image_format() returns the live swapchain format as a Portal
48 * ImageFormat, suitable for constructing a matching TextureBuffer.
49 *
50 * Write semantics (compositing) are deferred to a future processor.
51 */
52class MAYAFLUX_API WindowContainer : public SignalSourceContainer {
53public:
54 /**
55 * @brief Construct from an existing managed window.
56 * @param window Live window whose surface will be addressed as NDData.
57 * @param frame_capacity Number of rendered images to retain in m_data.
58 * Defaults to 1 (current behaviour).
59 */
60 explicit WindowContainer(std::shared_ptr<Core::Window> window,
61 uint32_t frame_capacity = 60);
62
63 ~WindowContainer() override = default;
64
69
70 /**
71 * @brief The underlying window.
72 */
73 [[nodiscard]] std::shared_ptr<Core::Window> get_window() const { return m_window; }
74
75 /**
76 * @brief Portal ImageFormat corresponding to the live swapchain surface format.
77 *
78 * Derived from the actual negotiated swapchain format via DisplayService,
79 * not from the WindowCreateInfo declaration. Use this when constructing a
80 * TextureBuffer to receive the output of to_image().
81 */
82 [[nodiscard]] Portal::Graphics::ImageFormat get_image_format() const;
83
84 /**
85 * @brief Mutable pointer into m_data[frame_index] for the processor to write into.
86 * @param frame_index Slot index in [0, frame_capacity).
87 * @return Pointer to the pixel buffer, or nullptr if out of range or unallocated.
88 */
89 [[nodiscard]] uint8_t* mutable_frame_ptr(uint32_t frame_index);
90
91 [[nodiscard]] uint32_t get_frame_capacity() const { return m_frame_capacity; }
92
93 /**
94 * @brief Current write head index in m_data, advanced by the default processor after each readback.
95 * Exposed for testing and potential future use by custom processors.
96 */
97 [[nodiscard]] uint32_t get_write_head() const { return m_write_head.load(); }
98
99 /** @brief Advance the write head index, wrapping around frame_capacity. */
100 void advance_write_head();
101
102 // =========================================================================
103 // NDDimensionalContainer
104 // =========================================================================
105
106 [[nodiscard]] std::vector<DataDimension> get_dimensions() const override;
107 [[nodiscard]] uint64_t get_total_elements() const override;
108 [[nodiscard]] MemoryLayout get_memory_layout() const override;
109 void set_memory_layout(MemoryLayout layout) override;
110
111 /**
112 * @brief Extract data for all regions across all region groups that
113 * spatially intersect @p region.
114 * Crops from the last full-surface readback — no GPU work.
115 * Returns empty if no readback has been performed yet.
116 */
117 [[nodiscard]] std::vector<DataVariant> get_region_data(const Region& region) const override;
118
119 /**
120 * @brief Upload the full surface readback to a new VKImage.
121 *
122 * Requires at least one completed readback (processed_data[0] non-empty
123 * and of type vector<uint8_t>). A fresh VKImage is created and uploaded
124 * on each call via TextureLoom; callers driving a per-frame path should
125 * prefer the staging-buffer overload to avoid per-call VkBuffer churn.
126 *
127 * @return Newly created VKImage, or nullptr on failure.
128 */
129 [[nodiscard]] std::shared_ptr<Core::VKImage> to_image() const;
130
131 /**
132 * @brief Upload the full surface readback to a new VKImage, reusing a
133 * caller-supplied persistent staging buffer.
134 *
135 * Allocates the VKImage without pixel data, then uploads via the
136 * provided staging buffer, bypassing the per-call VkBuffer allocation
137 * inside TextureLoom. Use TextureLoom::create_streaming_staging() to
138 * allocate the staging buffer once before the render loop.
139 *
140 * @param staging Host-visible staging VKBuffer sized to at least
141 * width * height * bytes_per_pixel.
142 * @return Newly created VKImage, or nullptr on failure.
143 */
144 [[nodiscard]] std::shared_ptr<Core::VKImage> to_image(
145 const std::shared_ptr<Buffers::VKBuffer>& staging) const;
146
147 /**
148 * @brief Upload m_data[frame_index] to a new VKImage.
149 * @param frame_index Index into m_data in [0, frame_capacity).
150 */
151 [[nodiscard]] std::shared_ptr<Core::VKImage> image_at(uint32_t frame_index) const;
152
153 /**
154 * @brief Upload m_data[frame_index] reusing a caller-supplied staging buffer.
155 * @param staging Host-visible VKBuffer sized to at least w * h * bpp.
156 * @param frame_index Index into m_data in [0, frame_capacity).
157 */
158 [[nodiscard]] std::shared_ptr<Core::VKImage> image_at(
159 const std::shared_ptr<Buffers::VKBuffer>& staging,
160 uint32_t frame_index) const;
161
162 /**
163 * @brief Crop a region from the last readback and upload it as a VKImage.
164 *
165 * Performs a CPU-side crop via extract_region_data; no additional GPU
166 * work beyond the readback that populated processed_data[0]. Region
167 * coordinates follow IMAGE_COLOR convention: [SPATIAL_Y, SPATIAL_X].
168 * The returned image dimensions are derived from the region extent.
169 *
170 * @param region Pixel rectangle. Must have at least 2 coordinates.
171 * @return VKImage sized to the region, or nullptr on failure.
172 */
173 [[nodiscard]] std::shared_ptr<Core::VKImage> region_to_image(const Region& region) const;
174
175 void set_region_data(const Region& region, const std::vector<DataVariant>& data) override;
176
177 [[nodiscard]] std::vector<DataVariant> get_region_group_data(const RegionGroup& group) const override;
178 [[nodiscard]] std::vector<DataVariant> get_segments_data(const std::vector<RegionSegment>& segments) const override;
179
180 [[nodiscard]] double get_value_at(const std::vector<uint64_t>& coordinates) const override;
181 void set_value_at(const std::vector<uint64_t>& coordinates, double value) override;
182
183 [[nodiscard]] uint64_t coordinates_to_linear_index(const std::vector<uint64_t>& coordinates) const override;
184 [[nodiscard]] std::vector<uint64_t> linear_index_to_coordinates(uint64_t linear_index) const override;
185
186 void clear() override;
187 void lock() override;
188 void unlock() override;
189 bool try_lock() override;
190
191 [[nodiscard]] const void* get_raw_data() const override;
192 [[nodiscard]] bool has_data() const override;
193
194 [[nodiscard]] ContainerDataStructure& get_structure() override { return m_structure; }
195 [[nodiscard]] const ContainerDataStructure& get_structure() const override { return m_structure; }
196 void set_structure(ContainerDataStructure s) override { m_structure = std::move(s); }
197
198 // -------------------------------------------------------------------------
199 // RegionGroup API — primary region registration interface.
200 // -------------------------------------------------------------------------
201
202 void add_region_group(const RegionGroup& group) override;
203 [[nodiscard]] const RegionGroup& get_region_group(const std::string& name) const override;
204 [[nodiscard]] std::unordered_map<std::string, RegionGroup> get_all_region_groups() const override;
205 void remove_region_group(const std::string& name) override;
206
207 /**
208 * @brief No-op. All surface data is continuously available after readback.
209 * Register regions via add_region_group() instead.
210 */
211 void load_region(const Region& region) override;
212
213 /**
214 * @brief No-op. See load_region().
215 */
216 void unload_region(const Region& region) override;
217
218 /**
219 * @brief Always returns true. Surface data is available after any readback.
220 */
221 [[nodiscard]] bool is_region_loaded(const Region& region) const override;
222
223 // =========================================================================
224 // SignalSourceContainer
225 // =========================================================================
226
227 [[nodiscard]] ProcessingState get_processing_state() const override;
228 void update_processing_state(ProcessingState new_state) override;
229
230 void register_state_change_callback(
231 std::function<void(const std::shared_ptr<SignalSourceContainer>&, ProcessingState)> callback) override;
232 void unregister_state_change_callback() override;
233
234 [[nodiscard]] bool is_ready_for_processing() const override;
235 void mark_ready_for_processing(bool ready) override;
236
237 void create_default_processor() override;
238 void process_default() override;
239
240 void set_default_processor(const std::shared_ptr<DataProcessor>& processor) override;
241 [[nodiscard]] std::shared_ptr<DataProcessor> get_default_processor() const override;
242
243 [[nodiscard]] std::shared_ptr<DataProcessingChain> get_processing_chain() override;
244 void set_processing_chain(const std::shared_ptr<DataProcessingChain>& chain) override;
245
246 [[nodiscard]] uint64_t get_frame_size() const override;
247 [[nodiscard]] uint64_t get_num_frames() const override;
248 [[nodiscard]] std::span<const double> get_frame(uint64_t frame_index) const override;
249 void get_frames(std::span<double> output, uint64_t start_frame, uint64_t num_frames) const override;
250
251 // -------------------------------------------------------------------------
252 // Consumer tracking — dimension_index and reader_id are opaque slot handles.
253 // Tracks whether all registered consumers have read processed_data[0] this
254 // cycle. The slot index argument exists for interface compliance; internally
255 // this container tracks a single consumer count across all slots.
256 // -------------------------------------------------------------------------
257 uint32_t register_dimension_reader(uint32_t dimension_index) override;
258 void unregister_dimension_reader(uint32_t dimension_index) override;
259 [[nodiscard]] bool has_active_readers() const override;
260 void mark_dimension_consumed(uint32_t dimension_index, uint32_t reader_id) override;
261 [[nodiscard]] bool all_dimensions_consumed() const override;
262
263 [[nodiscard]] std::vector<DataVariant>& get_processed_data() override;
264 [[nodiscard]] const std::vector<DataVariant>& get_processed_data() const override;
265 [[nodiscard]] const std::vector<DataVariant>& get_data() override;
266
267 void mark_buffers_for_processing(bool) override { }
268 void mark_buffers_for_removal() override { }
269
270 [[nodiscard]] DataAccess channel_data(size_t channel_index) override;
271 [[nodiscard]] std::vector<DataAccess> all_channel_data() override;
272
273private:
274 std::shared_ptr<Core::Window> m_window;
275
277 std::vector<DataVariant> m_data;
278 std::vector<DataVariant> m_processed_data;
279
280 std::unordered_map<std::string, RegionGroup> m_region_groups;
281
282 std::shared_ptr<DataProcessor> m_default_processor;
283 std::shared_ptr<DataProcessingChain> m_processing_chain;
284
285 std::atomic<ProcessingState> m_processing_state { ProcessingState::IDLE };
286 std::atomic<bool> m_ready_for_processing { false };
287
288 std::function<void(const std::shared_ptr<SignalSourceContainer>&, ProcessingState)> m_state_callback;
289
290 mutable std::shared_mutex m_data_mutex;
291 mutable std::mutex m_state_mutex;
292
293 mutable std::vector<double> m_frame_cache;
294
295 std::atomic<uint32_t> m_registered_readers { 0 };
296 std::atomic<uint32_t> m_consumed_readers { 0 };
297 std::atomic<uint32_t> m_next_reader_id { 0 };
298 std::atomic<uint32_t> m_write_head { 0 };
299 uint32_t m_frame_capacity { 1 };
300 std::atomic<uint64_t> m_frames_written { 0 };
301
302 void setup_dimensions();
303};
304
305} // namespace MayaFlux::Kakshya
Type-erased accessor for NDData with semantic view construction.
Data-driven interface for managing arbitrary processable signal sources.
std::function< void(const std::shared_ptr< SignalSourceContainer > &, ProcessingState)> m_state_callback
~WindowContainer() override=default
void mark_buffers_for_removal() override
Mark associated buffers for removal from the system.
WindowContainer & operator=(const WindowContainer &)=delete
WindowContainer(WindowContainer &&)=delete
std::shared_ptr< Core::Window > m_window
ContainerDataStructure & get_structure() override
Get the data structure defining this container's layout.
WindowContainer & operator=(WindowContainer &&)=delete
uint32_t get_write_head() const
Current write head index in m_data, advanced by the default processor after each readback.
std::vector< DataVariant > m_processed_data
std::unordered_map< std::string, RegionGroup > m_region_groups
std::vector< DataVariant > m_data
std::shared_ptr< DataProcessingChain > m_processing_chain
std::shared_ptr< DataProcessor > m_default_processor
void set_structure(ContainerDataStructure s) override
Set the data structure for this container.
WindowContainer(const WindowContainer &)=delete
void mark_buffers_for_processing(bool) override
Mark associated buffers for processing in the next cycle.
std::shared_ptr< Core::Window > get_window() const
The underlying window.
const ContainerDataStructure & get_structure() const override
SignalSourceContainer wrapping a live GLFW/Vulkan window surface.
ProcessingState
Represents the current processing lifecycle state of a container.
std::optional< RegionGroup > get_region_group(const std::unordered_map< std::string, RegionGroup > &groups, const std::string &name)
Get a RegionGroup by name from a group map.
void add_region_group(std::unordered_map< std::string, RegionGroup > &groups, const RegionGroup &group)
Add a RegionGroup to a group map.
MemoryLayout
Memory layout for multi-dimensional data.
Definition NDData.hpp:39
void remove_region_group(std::unordered_map< std::string, RegionGroup > &groups, const std::string &name)
Remove a RegionGroup by name from a group map.
ImageFormat
User-friendly image format enum.
Container structure for consistent dimension ordering.
Organizes related signal regions into a categorized collection.
Represents a point or span in N-dimensional space.
Definition Region.hpp:73