Files
CoopAllTheThings/common/include/coop/log_ring.hpp
BlackMark cb6749b511 Audio: robust rate estimation + color-coded log levels
Harden the guessed-stream sample-rate measurement that produced wrong rates
(e.g. 44100 read as ~46205). New rate_estimator.hpp measures over longer
~0.5 s windows, rejects any window that doesn't snap to a standard rate
(standard rates are >8% apart, so a quantization/burst error big enough to
miss one lands in no-man's-land, never on a wrong neighbour), and requires
consensus across windows before committing. If consensus isn't reached it
commits a low-confidence estimate (new AudioFormat_LowConfidence, shown red)
rather than spinning or publishing garbage. Pure logic, unit-tested with
adversarial cadences (rate_estimator_test) incl. the real 46205 bug value.

Add log severity levels: hook logw/loge set LogRecord.level; the host Log
window colors warnings amber and errors red. The low-confidence rate logs a
warning. Protocol -> v15 (new format states); also reserves AudioFormat_Override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 01:11:06 +02:00

139 lines
4.5 KiB
C++

// Shared-memory log channel: the injected hook (coop_hook.dll) streams its log
// lines to the host (coop_host.exe), which shows them in a Log window. Separate
// mapping from the input/status SharedBlock, named coop_log_<pid>.
//
// Lossy multi-producer / single-consumer ring: the hook logs from several threads
// (worker, audio render thread, window thread), so producers claim a slot with an
// atomic fetch_add and publish each record with a release store of its sequence;
// the host consumer reads in order and tolerates losing the oldest lines if it
// ever falls a whole ring behind (fine for diagnostics). POD + version-locked.
#pragma once
#include <atomic>
#include <cstdint>
#include <cstring>
#include <string>
namespace coop
{
// 'CLOG' little-endian.
inline constexpr std::uint32_t kLogRingMagic = 0x474F4C43u;
inline constexpr std::uint32_t kLogRingVersion = 1;
// Per-pid mapping name, mirroring the other channels: coop_log_<pid>.
inline constexpr wchar_t kLogRingPrefix[] = L"Local\\coop_log_";
inline constexpr std::uint32_t kLogMsgLen = 192; // chars per line (incl. NUL)
inline constexpr std::uint32_t kLogCapacity = 1024; // ring records
// Severity of a log line; drives the host Log window's colour. Stored in
// LogRecord::level. Info is 0 so existing/zero-filled records read as Info.
enum LogLevel : std::uint32_t
{
LogLevel_Info = 0,
LogLevel_Warn = 1,
LogLevel_Error = 2,
};
struct LogRecord
{
std::atomic<std::uint64_t> seq; // 0 = empty; else (global index + 1) once written
std::uint32_t pid;
std::uint32_t level; // LogLevel
std::uint64_t millis; // producer timestamp (GetTickCount64)
char text[kLogMsgLen];
};
struct LogRing
{
std::uint32_t magic;
std::uint32_t version;
std::uint32_t capacity; // number of records
std::uint32_t msg_len; // kLogMsgLen (sanity)
std::atomic<std::uint64_t> write_index; // total records ever claimed (free-running)
std::uint8_t reserved[32];
// LogRecord records[capacity] follows immediately.
};
static_assert(std::atomic<std::uint64_t>::is_always_lock_free,
"log ring needs a lock-free 64-bit atomic for cross-process use");
inline constexpr std::size_t log_ring_total_size(std::uint32_t capacity)
{
return sizeof(LogRing) + static_cast<std::size_t>(capacity) * sizeof(LogRecord);
}
inline LogRecord* log_ring_records(LogRing* r)
{
return reinterpret_cast<LogRecord*>(reinterpret_cast<std::uint8_t*>(r) + sizeof(LogRing));
}
// Host: stamp a freshly created (zero-filled) mapping. Records start empty (seq 0).
inline void log_ring_init(LogRing& r, std::uint32_t capacity)
{
r.capacity = capacity;
r.msg_len = kLogMsgLen;
r.write_index.store(0, std::memory_order_relaxed);
std::memset(r.reserved, 0, sizeof(r.reserved));
r.version = kLogRingVersion;
r.magic = kLogRingMagic; // last
}
inline bool log_ring_valid(const LogRing& r)
{
return r.magic == kLogRingMagic && r.version == kLogRingVersion && r.capacity != 0 &&
r.msg_len == kLogMsgLen;
}
// Producer (hook): append a line at severity `level` (LogLevel). Multi-producer safe.
inline void log_ring_push(LogRing& r, std::uint32_t pid, std::uint32_t level, std::uint64_t millis,
const char* text)
{
const std::uint64_t idx = r.write_index.fetch_add(1, std::memory_order_acq_rel);
LogRecord& rec = log_ring_records(&r)[idx % r.capacity];
rec.pid = pid;
rec.level = level;
rec.millis = millis;
std::strncpy(rec.text, text, kLogMsgLen - 1);
rec.text[kLogMsgLen - 1] = '\0';
rec.seq.store(idx + 1, std::memory_order_release); // publish: record is ready
}
// Consumer (host): emit each new record since `cursor` (advanced in place). Skips
// records lost to ring wrap; stops at an in-flight record and retries next call.
template <typename F>
inline void log_ring_drain(LogRing& r, std::uint64_t& cursor, F&& emit)
{
const std::uint64_t w = r.write_index.load(std::memory_order_acquire);
if (w <= cursor)
{
return;
}
const std::uint64_t lo = (w > r.capacity) ? (w - r.capacity) : 0;
std::uint64_t i = cursor < lo ? lo : cursor; // skip records already overwritten
LogRecord* recs = log_ring_records(&r);
for (; i < w; ++i)
{
LogRecord& rec = recs[i % r.capacity];
const std::uint64_t s = rec.seq.load(std::memory_order_acquire);
if (s == i + 1)
{
emit(rec); // ready
}
else if (s <= i)
{
break; // slot not written for this generation yet (in-flight); retry later
}
// s > i + 1: overwritten before we read it; skip (lost)
}
cursor = i;
}
inline std::wstring log_ring_name(unsigned long target_pid)
{
return std::wstring(kLogRingPrefix) + std::to_wstring(target_pid);
}
} // namespace coop