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