// Shared-memory audio ring for the injection render-hook audio path. // // The injected hook (coop_hook.dll) captures the game's WASAPI render frames and // is the sole *producer*; the host (coop_host.exe) is the sole *consumer* and // re-renders the frames for Steam Remote Play Together. This is a separate, // larger mapping from the input/status SharedBlock (which is only 20-byte pads // and can't hold PCM): a header followed by a byte ring of `capacity` bytes. // // Lock-free SPSC with free-running 64-bit positions (release on publish, acquire // on read) — the same cross-process atomic model as the input seqlock. POD and // version-locked: both modules compile this identical header. #pragma once #include #include #include #include #include namespace coop { // 'AURG' little-endian; sanity-checks the mapping before either side trusts it. inline constexpr std::uint32_t kAudioRingMagic = 0x47525541u; // Bump whenever AudioRingHeader's layout changes. inline constexpr std::uint32_t kAudioRingVersion = 1; // Per-pid mapping name, mirroring kSharedMemoryPrefix: coop_audio_. inline constexpr wchar_t kAudioRingPrefix[] = L"Local\\coop_audio_"; // Byte capacity of the PCM ring. 1 MiB is >1 s even at 48 kHz / 2 ch / 32-bit // float (384 kB/s); the host should always keep up, so this is pure slack. inline constexpr std::uint32_t kAudioRingCapacity = 1u << 20; // Header preceding the PCM data. All multi-process-shared counters are atomic; // the format fields are written once by the producer *before* it publishes // format_valid (release), and read by the consumer *after* it observes // format_valid (acquire), so they need no atomicity of their own. struct AudioRingHeader { std::uint32_t magic; std::uint32_t version; // Host-owned gate. The hook copies+silences frames only while this is 1; // when 0 the game audio passes through locally and nothing is mirrored // (stream counting in HookStatus still runs regardless of this flag). std::atomic capture_enabled; // Producer publishes the captured stream's format once, then sets // format_valid=1 (release). format_generation is reserved so a future // mid-session device re-init can be made forward-compatible; v1 sets once. std::atomic format_valid; std::atomic format_generation; std::uint32_t sample_rate; std::uint32_t channels; std::uint32_t bits; std::uint32_t format_tag; // WAVE_FORMAT_* (PCM=1, IEEE_FLOAT=3, EXTENSIBLE=0xFFFE) std::uint32_t block_align; // bytes per frame (all channels) std::uint32_t capacity; // bytes in the trailing data region std::atomic write_pos; // producer cursor, free-running std::atomic read_pos; // consumer cursor, free-running std::atomic frames_produced; // cumulative frames pushed std::atomic overruns; // packets dropped on a full ring std::uint8_t reserved[64]; // std::uint8_t data[capacity] follows immediately in the mapping. }; static_assert(std::atomic::is_always_lock_free, "audio ring needs a lock-free 64-bit atomic for cross-process use"); // Total mapping size for a ring of `capacity` bytes. inline constexpr std::size_t audio_ring_total_size(std::uint32_t capacity) { return sizeof(AudioRingHeader) + capacity; } // Pointer to the PCM data region following the header. inline std::uint8_t* audio_ring_data(AudioRingHeader* h) { return reinterpret_cast(h) + sizeof(AudioRingHeader); } // Host side: stamp a freshly created mapping into a valid empty ring. inline void audio_ring_init(AudioRingHeader& h, std::uint32_t capacity) { h.magic = kAudioRingMagic; h.version = kAudioRingVersion; h.capture_enabled.store(0, std::memory_order_relaxed); h.format_valid.store(0, std::memory_order_relaxed); h.format_generation.store(0, std::memory_order_relaxed); h.sample_rate = 0; h.channels = 0; h.bits = 0; h.format_tag = 0; h.block_align = 0; h.capacity = capacity; h.write_pos.store(0, std::memory_order_relaxed); h.read_pos.store(0, std::memory_order_relaxed); h.frames_produced.store(0, std::memory_order_relaxed); h.overruns.store(0, std::memory_order_relaxed); std::memset(h.reserved, 0, sizeof(h.reserved)); } // Validate a mapping the other side created/opened. inline bool audio_ring_valid(const AudioRingHeader& h) { return h.magic == kAudioRingMagic && h.version == kAudioRingVersion && h.capacity != 0; } // Producer (hook): publish the captured stream format, then mark it valid. inline void audio_ring_set_format(AudioRingHeader& h, std::uint32_t sample_rate, std::uint32_t channels, std::uint32_t bits, std::uint32_t format_tag, std::uint32_t block_align) { h.sample_rate = sample_rate; h.channels = channels; h.bits = bits; h.format_tag = format_tag; h.block_align = block_align; h.format_generation.fetch_add(1, std::memory_order_relaxed); h.format_valid.store(1, std::memory_order_release); } // Consumer (host): true once the producer has published a format. inline bool audio_ring_format_ready(const AudioRingHeader& h) { return h.format_valid.load(std::memory_order_acquire) != 0; } // Producer: push `bytes` of PCM. Returns false (and bumps overruns) if the ring // can't hold the whole packet, in which case nothing is written — drop the // packet rather than tear a frame. `frames` is recorded for the diagnostics. inline bool audio_ring_push(AudioRingHeader& h, const void* src, std::uint32_t bytes, std::uint32_t frames) { const std::uint64_t w = h.write_pos.load(std::memory_order_relaxed); const std::uint64_t r = h.read_pos.load(std::memory_order_acquire); const std::uint32_t used = static_cast(w - r); if (bytes > h.capacity - used) { h.overruns.fetch_add(1, std::memory_order_relaxed); return false; } std::uint8_t* data = audio_ring_data(&h); const std::uint32_t off = static_cast(w % h.capacity); const std::uint32_t first = std::min(bytes, h.capacity - off); std::memcpy(data + off, src, first); if (bytes > first) { std::memcpy(data, static_cast(src) + first, bytes - first); } h.write_pos.store(w + bytes, std::memory_order_release); h.frames_produced.fetch_add(frames, std::memory_order_relaxed); return true; } // Consumer: bytes currently available to read. inline std::uint32_t audio_ring_available(const AudioRingHeader& h) { const std::uint64_t w = h.write_pos.load(std::memory_order_acquire); const std::uint64_t r = h.read_pos.load(std::memory_order_relaxed); return static_cast(w - r); } // Consumer: copy up to `bytes` into `dst`; returns the number actually popped. inline std::uint32_t audio_ring_pop(AudioRingHeader& h, void* dst, std::uint32_t bytes) { const std::uint64_t r = h.read_pos.load(std::memory_order_relaxed); const std::uint64_t w = h.write_pos.load(std::memory_order_acquire); const std::uint32_t avail = static_cast(w - r); bytes = std::min(bytes, avail); const std::uint8_t* data = audio_ring_data(&h); const std::uint32_t off = static_cast(r % h.capacity); const std::uint32_t first = std::min(bytes, h.capacity - off); std::memcpy(dst, data + off, first); if (bytes > first) { std::memcpy(static_cast(dst) + first, data, bytes - first); } h.read_pos.store(r + bytes, std::memory_order_release); return bytes; } // Build the per-pid audio ring name both sides agree on. Stream 0 keeps the bare // coop_audio_ name (backward compatible / the single-stream case); additional // streams append _ (coop_audio__1, _2, ...). The host captures every // render stream into its own ring and mixes them. inline std::wstring audio_ring_name(unsigned long target_pid, unsigned index = 0) { std::wstring name = std::wstring(kAudioRingPrefix) + std::to_wstring(target_pid); if (index != 0) { name += L"_" + std::to_wstring(index); } return name; } } // namespace coop