diff --git a/.agents/MEMORY.md b/.agents/MEMORY.md index eea7692..2344a61 100644 --- a/.agents/MEMORY.md +++ b/.agents/MEMORY.md @@ -4,12 +4,10 @@ Last updated: initial scaffold. ## Project state -- Empty project scaffolded with Meson build, C++20, module headers, and agent - workflow files. -- No implementation yet; headers are placeholders. -- Phase plan is in `docs/PHASES.md`. Agents are now explicitly instructed in - `AGENTS.md` to read `docs/PHASES.md` before planning and not to start work - belonging to a later phase unless requested. +- Phase 1 complete: Meson build, C++20 module headers, agent workflow, clock + utility, and a passing unit test. +- Active phase is now Phase 2 (software H.264 encode/decode). +- No implementation yet for capture/codec/network/render. ## Decisions diff --git a/README.md b/README.md index 413d9f6..fc07611 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ A native Linux peer-to-peer screencast application. Requirements: - C++20 compiler (GCC 12+, Clang 16+) -- Meson + Ninja +- Meson >= 0.63 +- Ninja - (later phases) FFmpeg dev packages, PipeWire dev, SDL2/3 dev Build and run tests: @@ -25,6 +26,12 @@ meson compile -C build meson test -C build --print-errorlogs ``` +Run with verbose test output: + +```sh +meson test -C build -v +``` + ## Architecture See `docs/ARCHITECTURE.md` for module boundaries and design rules. @@ -33,7 +40,7 @@ See `docs/ARCHITECTURE.md` for module boundaries and design rules. Development is split into phases in `docs/PHASES.md`. -Current phase: **Phase 1 — project skeleton**. +Current phase: **Phase 2 — software H.264 encode / decode**. ## License diff --git a/docs/PHASES.md b/docs/PHASES.md index 19dffae..3f9302f 100644 --- a/docs/PHASES.md +++ b/docs/PHASES.md @@ -10,8 +10,8 @@ previous one is validated. - [x] Meson build files (`meson.build`, `meson_options.txt`). - [x] Public module headers with `namespace sc`. -- [ ] Unit test that exercises a trivial utility function. -- [ ] `README.md` with build instructions. +- [x] Unit test that exercises a trivial utility function. +- [x] `README.md` with build instructions. **Validation**: `meson setup build && meson compile -C build && meson test -C build`. @@ -89,4 +89,4 @@ where available. ## Current phase -Phase 1 — skeleton and tooling. +Phase 2 — software H.264 encode/decode. diff --git a/include/screencast/utils/clock.h b/include/screencast/utils/clock.h new file mode 100644 index 0000000..8cfc629 --- /dev/null +++ b/include/screencast/utils/clock.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace sc { + +// RTP clock rate for video is 90 kHz. Convert a monotonic nanosecond timestamp +// into a 32-bit RTP timestamp. Overflows wrap around intentionally. +constexpr uint32_t rtp_timestamp_from_ns ( uint64_t nanoseconds ) noexcept { + // (nanoseconds * 90_000) / 1_000_000_000 + // Simplify: 90_000 / 1_000_000_000 = 9 / 100_000 + return static_cast ( ( nanoseconds * 9ULL ) / 100'000ULL ); +} + +static_assert ( rtp_timestamp_from_ns ( 0 ) == 0 ); +static_assert ( rtp_timestamp_from_ns ( 1'000'000'000ULL ) == 90'000 ); +static_assert ( rtp_timestamp_from_ns ( 500'000'000ULL ) == 45'000 ); + +} // namespace sc diff --git a/meson.build b/meson.build index 933e6f7..591f259 100644 --- a/meson.build +++ b/meson.build @@ -10,8 +10,7 @@ project('screen_cast', 'cpp', # Public and private include directories inc = include_directories('include') -# Build the core static library first; dependencies will be added later as -# phases require them. +# Core public headers / include dependency defined in src/meson.build. subdir('src') # Tests diff --git a/src/meson.build b/src/meson.build index adad535..b392b3e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,2 +1,6 @@ -# Empty stub: implementation files will be added phase by phase. -# For now the project configures successfully with no targets. +# Core public API is currently header-only. Source files will be added as +# modules are implemented in later phases. + +# Declare an include-only dependency so that tests and executables can depend on +# the public headers consistently. +sc_core_inc = include_directories('../include') diff --git a/tests/meson.build b/tests/meson.build index 1a29ca8..0b1b499 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1 +1,5 @@ -# Empty stub: tests will be added as modules land. +test_clock = executable('test_clock', + 'utils/test_clock.cpp', + include_directories : sc_core_inc) + +test('clock utils', test_clock) diff --git a/tests/utils/test_clock.cpp b/tests/utils/test_clock.cpp new file mode 100644 index 0000000..77e5860 --- /dev/null +++ b/tests/utils/test_clock.cpp @@ -0,0 +1,20 @@ +#include "screencast/utils/clock.h" + +#include +#include + +int main () { + assert ( sc::rtp_timestamp_from_ns ( 0 ) == 0u ); + assert ( sc::rtp_timestamp_from_ns ( 1'000'000'000ULL ) == 90'000u ); + assert ( sc::rtp_timestamp_from_ns ( 500'000'000ULL ) == 45'000u ); + assert ( sc::rtp_timestamp_from_ns ( 16'666'667ULL ) == 1'500u ); + + // A multiple of the wrap period should wrap back to zero. + // Period in ns = ceil(2^32 * 100_000 / 9) because the helper uses + // integer floor division (ns * 9 / 100_000). + constexpr uint64_t period_ns = ( ( 1ULL << 32 ) * 100'000ULL + 9ULL - 1ULL ) / 9ULL; + static_assert ( sc::rtp_timestamp_from_ns ( period_ns ) == 0u ); + static_assert ( sc::rtp_timestamp_from_ns ( period_ns + 1'000'000'000ULL ) == 90'000u ); + + return 0; +}