Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/project_policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Version:** 1.0
**Status:** Active
**Last Updated:** 2026-01-07
**Last Updated:** 2026-01-15

This document establishes mandatory project-wide development policies for the Goggles codebase. All contributors must follow these rules to ensure consistency, maintainability, and quality.

Expand Down Expand Up @@ -277,6 +277,16 @@ Over-commenting causes:
3. **False confidence** - outdated comments mislead
4. **Crutch for bad code** - encourages unclear implementations

### C.8 Docstrings (Public API)

Docstrings are for API contracts, not implementation narration.

- Use Doxygen line comments (`///` / `///<`) on exported/public declarations only.
- Keep `@brief` to 1–2 sentences: active voice, start with a verb, end with a period.
- Use `@param <name>` for each parameter; describe purpose plus units/range/constraints when relevant.
- Use `@return` for non-`void`; describe what it represents and any special cases.
- Do not restate types, avoid jargon, and keep one symbol per docstring.

---

## D) Ownership & Resource Lifetime Rules
Expand Down
8 changes: 8 additions & 0 deletions src/capture/capture_protocol.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

namespace goggles::capture {

/// @brief Abstract socket path used by the capture layer and receiver.
// NOLINTNEXTLINE(modernize-avoid-c-arrays) - abstract socket path requires C array
constexpr const char CAPTURE_SOCKET_PATH[] = "\0goggles/vkcapture";
constexpr size_t CAPTURE_SOCKET_PATH_LEN = sizeof(CAPTURE_SOCKET_PATH) - 1;

/// @brief Message types used on the capture IPC protocol.
// NOLINTNEXTLINE(performance-enum-size) - uint32_t required for wire format stability
enum class CaptureMessageType : uint32_t {
client_hello = 1,
Expand All @@ -20,6 +22,7 @@ enum class CaptureMessageType : uint32_t {
resolution_response = 6,
};

/// @brief Initial handshake message sent by the client.
struct CaptureClientHello {
CaptureMessageType type = CaptureMessageType::client_hello;
uint32_t version = 1;
Expand All @@ -28,6 +31,7 @@ struct CaptureClientHello {

static_assert(sizeof(CaptureClientHello) == 72);

/// @brief Legacy texture metadata sent alongside an exported DMA-BUF FD.
struct CaptureTextureData {
CaptureMessageType type = CaptureMessageType::texture_data;
uint32_t width = 0;
Expand All @@ -40,9 +44,11 @@ struct CaptureTextureData {

static_assert(sizeof(CaptureTextureData) == 32);

/// @brief Control flags for `CaptureControl::flags`.
constexpr uint32_t CAPTURE_CONTROL_CAPTURING = 1u << 0;
constexpr uint32_t CAPTURE_CONTROL_RESOLUTION_REQUEST = 1u << 1;

/// @brief Control message sent from receiver to client.
struct CaptureControl {
CaptureMessageType type = CaptureMessageType::control;
uint32_t flags = 0;
Expand All @@ -52,6 +58,7 @@ struct CaptureControl {

static_assert(sizeof(CaptureControl) == 16);

/// @brief Initializes timeline semaphore synchronization via SCM_RIGHTS FD passing.
struct CaptureSemaphoreInit {
CaptureMessageType type = CaptureMessageType::semaphore_init;
uint32_t version = 1;
Expand All @@ -61,6 +68,7 @@ struct CaptureSemaphoreInit {

static_assert(sizeof(CaptureSemaphoreInit) == 16);

/// @brief Per-frame metadata for virtual frame forwarding.
struct CaptureFrameMetadata {
CaptureMessageType type = CaptureMessageType::frame_metadata;
uint32_t width = 0;
Expand Down
19 changes: 19 additions & 0 deletions src/capture/capture_receiver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace goggles {

/// @brief Latest captured frame metadata plus DMA-BUF handle.
struct CaptureFrame {
uint32_t width = 0;
uint32_t height = 0;
Expand All @@ -20,31 +21,49 @@ struct CaptureFrame {
uint64_t frame_number = 0;
};

/// @brief Receives capture frames over the local IPC protocol.
class CaptureReceiver {
public:
/// @brief Creates and starts a capture receiver.
/// @return A receiver or an error.
[[nodiscard]] static auto create() -> ResultPtr<CaptureReceiver>;

~CaptureReceiver();

CaptureReceiver(const CaptureReceiver&) = delete;
CaptureReceiver& operator=(const CaptureReceiver&) = delete;

/// @brief Shuts down sockets and clears any held frame state.
void shutdown();
/// @brief Polls the socket and updates internal state if a new frame arrives.
/// @return True if progress was made, false if disconnected or no data.
bool poll_frame();
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
/// @brief Requests the capture client to change resolution.
/// @param width Requested width in pixels.
/// @param height Requested height in pixels.
void request_resolution(uint32_t width, uint32_t height);

/// @brief Returns the most recent frame metadata.
[[nodiscard]] const CaptureFrame& get_frame() const { return m_frame; }
/// @brief Returns true if a client is connected.
[[nodiscard]] bool is_connected() const { return m_client_fd >= 0; }
/// @brief Returns true if a frame DMA-BUF FD is available.
[[nodiscard]] bool has_frame() const { return m_frame.dmabuf_fd.valid(); }

/// @brief Returns the "frame ready" semaphore FD, or `-1` if unavailable.
[[nodiscard]] int get_frame_ready_fd() const { return m_frame_ready_fd; }
/// @brief Returns the "frame consumed" semaphore FD, or `-1` if unavailable.
[[nodiscard]] int get_frame_consumed_fd() const { return m_frame_consumed_fd; }
/// @brief Returns true if both sync semaphore FDs are available.
[[nodiscard]] bool has_sync_semaphores() const {
return m_frame_ready_fd >= 0 && m_frame_consumed_fd >= 0;
}
/// @brief Returns true if sync semaphore FDs changed since last clear.
[[nodiscard]] bool semaphores_updated() const { return m_semaphores_updated; }
/// @brief Clears the "semaphores updated" flag.
void clear_semaphores_updated() { m_semaphores_updated = false; }
/// @brief Clears and closes any held sync semaphore FDs.
void clear_sync_semaphores();

private:
Expand Down
12 changes: 9 additions & 3 deletions src/capture/compositor/compositor_capture.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
// Compositor/framebuffer capture backend - stub
#pragma once

namespace goggles::capture {

// Placeholder for compositor capture
/// @brief Compositor/framebuffer capture backend.
class CompositorCapture {
// Empty stub
public:
CompositorCapture() = default;
~CompositorCapture() = default;

CompositorCapture(const CompositorCapture&) = delete;
CompositorCapture& operator=(const CompositorCapture&) = delete;
CompositorCapture(CompositorCapture&&) = delete;
CompositorCapture& operator=(CompositorCapture&&) = delete;
};

} // namespace goggles::capture
22 changes: 22 additions & 0 deletions src/capture/vk_layer/ipc_socket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

namespace goggles::capture {

/// @brief Virtual resolution request received from the viewer.
struct ResolutionRequest {
bool pending = false;
uint32_t width = 0;
uint32_t height = 0;
};

/// @brief IPC client used by the Vulkan layer to send captured frames.
class LayerSocketClient {
public:
LayerSocketClient() = default;
Expand All @@ -21,15 +23,34 @@ class LayerSocketClient {
LayerSocketClient(const LayerSocketClient&) = delete;
LayerSocketClient& operator=(const LayerSocketClient&) = delete;

/// @brief Connects to the capture receiver socket.
/// @return True if connected.
bool connect();
/// @brief Disconnects and closes any held socket resources.
void disconnect();
/// @brief Returns true if currently connected.
bool is_connected() const;
/// @brief Sends texture metadata and an exported DMA-BUF FD.
/// @param data Texture metadata.
/// @param dmabuf_fd Exported DMA-BUF file descriptor.
/// @return True on success.
bool send_texture(const CaptureTextureData& data, int dmabuf_fd);
/// @brief Sends synchronization semaphore FDs via SCM_RIGHTS.
/// @return True on success.
bool send_semaphores(int frame_ready_fd, int frame_consumed_fd);
/// @brief Sends per-frame metadata and an exported DMA-BUF FD.
/// @return True on success.
bool send_texture_with_fd(const CaptureFrameMetadata& metadata, int dmabuf_fd);
/// @brief Sends per-frame metadata without transferring any FDs.
/// @return True on success.
bool send_frame_metadata(const CaptureFrameMetadata& metadata);
/// @brief Polls for control messages from the receiver.
/// @param control Output control message.
/// @return True on success.
bool poll_control(CaptureControl& control);
/// @brief Returns the last known capture-enabled state.
bool is_capturing() const;
/// @brief Consumes a pending resolution request, if any.
ResolutionRequest consume_resolution_request();

private:
Expand All @@ -42,6 +63,7 @@ class LayerSocketClient {
std::atomic<uint32_t> res_height_{0};
};

/// @brief Returns the process-wide layer socket client instance.
LayerSocketClient& get_layer_socket();

} // namespace goggles::capture
19 changes: 19 additions & 0 deletions src/capture/vk_layer/vk_capture.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@

namespace goggles::capture {

/// @brief Reusable copy command buffer state for exported swapchain images.
struct CopyCmd {
VkCommandPool pool = VK_NULL_HANDLE;
VkCommandBuffer cmd = VK_NULL_HANDLE;
uint64_t timeline_value = 0;
bool busy = false;
};

/// @brief Metadata for a virtual frame forwarded from a WSI proxy swapchain.
struct VirtualFrameInfo {
uint32_t width = 0;
uint32_t height = 0;
Expand All @@ -32,6 +34,7 @@ struct VirtualFrameInfo {
int dmabuf_fd = -1;
};

/// @brief Work item used by the capture worker thread.
struct AsyncCaptureItem {
VkDevice device = VK_NULL_HANDLE;
VkSemaphore timeline_sem = VK_NULL_HANDLE;
Expand All @@ -40,6 +43,7 @@ struct AsyncCaptureItem {
CaptureFrameMetadata metadata{};
};

/// @brief Device-level sync state independent of swapchain lifecycle.
// Device-level sync state (independent of swapchain lifecycle)
struct DeviceSyncState {
VkSemaphore frame_ready_sem = VK_NULL_HANDLE;
Expand All @@ -51,13 +55,15 @@ struct DeviceSyncState {
bool initialized = false;
};

/// @brief Snapshot of device sync state for lock-free reads.
struct DeviceSyncSnapshot {
VkSemaphore frame_consumed_sem = VK_NULL_HANDLE;
uint64_t frame_counter = 0;
bool semaphores_sent = false;
bool initialized = false;
};

/// @brief Per-swapchain capture bookkeeping and exported DMA-BUF resources.
struct SwapData {
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
Expand Down Expand Up @@ -89,23 +95,35 @@ struct SwapData {
bool dmabuf_sent = false;
};

/// @brief Manages swapchain capture, DMA-BUF export, and IPC forwarding.
class CaptureManager {
public:
CaptureManager();
~CaptureManager();

/// @brief Tracks a newly created swapchain and initializes capture resources as needed.
void on_swapchain_created(VkDevice device, VkSwapchainKHR swapchain,
const VkSwapchainCreateInfoKHR* create_info, VkDeviceData* dev_data);
/// @brief Cleans up capture resources for a destroyed swapchain.
void on_swapchain_destroyed(VkDevice device, VkSwapchainKHR swapchain);
/// @brief Cleans up device-level capture state.
void on_device_destroyed(VkDevice device, VkDeviceData* dev_data);
/// @brief Handles a present call and schedules capture work if enabled.
void on_present(VkQueue queue, const VkPresentInfoKHR* present_info, VkDeviceData* dev_data);
/// @brief Returns swapchain capture state, or null if untracked.
SwapData* get_swap_data(VkSwapchainKHR swapchain);
/// @brief Returns a snapshot of device sync state.
DeviceSyncSnapshot get_device_sync_snapshot(VkDevice device);
/// @brief Ensures device-level sync primitives exist for capture.
bool ensure_device_sync(VkDevice device, VkDeviceData* dev_data);
/// @brief Attempts to send device sync semaphore FDs to the receiver.
bool try_send_device_semaphores(VkDevice device);
/// @brief Returns a monotonically increasing counter for virtual frames.
uint64_t get_virtual_frame_counter() const;
/// @brief Stops background work and disconnects any IPC state.
void shutdown();

/// @brief Queues a virtual frame (from WSI proxy) for forwarding.
void enqueue_virtual_frame(const VirtualFrameInfo& frame);

private:
Expand Down Expand Up @@ -135,6 +153,7 @@ class CaptureManager {
std::atomic<uint64_t> virtual_frame_counter_{0};
};

/// @brief Returns the process-wide capture manager instance.
CaptureManager& get_capture_manager();

} // namespace goggles::capture
18 changes: 18 additions & 0 deletions src/capture/vk_layer/vk_dispatch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

namespace goggles::capture {

/// @brief Extracts the loader dispatch table pointer from a dispatchable Vulkan handle.
// First pointer in dispatchable Vulkan objects is the loader dispatch table
#define GET_LDT(x) (*(void**)(x))

/// @brief Instance-level Vulkan entry points used by the layer.
struct VkInstFuncs {
PFN_vkGetInstanceProcAddr GetInstanceProcAddr;
PFN_vkDestroyInstance DestroyInstance;
Expand All @@ -35,6 +37,7 @@ struct VkInstFuncs {
PFN_vkGetPhysicalDeviceSurfaceFormats2KHR GetPhysicalDeviceSurfaceFormats2KHR;
};

/// @brief Device-level Vulkan entry points used by the layer.
struct VkDeviceFuncs {
PFN_vkGetDeviceProcAddr GetDeviceProcAddr;
PFN_vkDestroyDevice DestroyDevice;
Expand Down Expand Up @@ -89,12 +92,14 @@ struct VkDeviceFuncs {
PFN_vkWaitSemaphoresKHR WaitSemaphoresKHR;
};

/// @brief Tracked instance state and dispatch table.
struct VkInstData {
VkInstance instance;
VkInstFuncs funcs;
bool valid;
};

/// @brief Tracked device state and dispatch table.
struct VkDeviceData {
VkDevice device;
VkPhysicalDevice physical_device;
Expand All @@ -105,21 +110,33 @@ struct VkDeviceData {
bool valid;
};

/// @brief Tracks instances/devices/queues to look up dispatch tables during hooking.
class ObjectTracker {
public:
/// @brief Adds instance tracking data.
void add_instance(VkInstance instance, VkInstData data);
/// @brief Returns instance tracking data, or null.
VkInstData* get_instance(VkInstance instance);
/// @brief Returns instance tracking data for a physical device, or null.
VkInstData* get_instance_by_physical_device(VkPhysicalDevice device);
/// @brief Removes instance tracking data.
void remove_instance(VkInstance instance);

/// @brief Adds device tracking data.
void add_device(VkDevice device, VkDeviceData data);
/// @brief Returns device tracking data, or null.
VkDeviceData* get_device(VkDevice device);
/// @brief Returns device tracking data for a queue, or null.
VkDeviceData* get_device_by_queue(VkQueue queue);
/// @brief Removes device tracking data.
void remove_device(VkDevice device);

/// @brief Records a queue-to-device association.
void add_queue(VkQueue queue, VkDevice device);
/// @brief Removes all queues associated with a device.
void remove_queues_for_device(VkDevice device);

/// @brief Records a physical-device-to-instance association.
void add_physical_device(VkPhysicalDevice phys, VkInstance inst);

private:
Expand All @@ -130,6 +147,7 @@ class ObjectTracker {
std::unordered_map<void*, VkInstance> phys_to_instance_;
};

/// @brief Returns the process-wide object tracker instance.
ObjectTracker& get_object_tracker();

} // namespace goggles::capture
Loading