diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..33bd0c7f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# Normalize text files by default, but only force EOL where tooling is sensitive. +* text=auto + +# Shell scripts must stay LF for bash on Linux/macOS/WSL. +*.sh text eol=lf +*.bash text eol=lf +*.zsh text eol=lf + +# CMake files are part of the cross-platform Unix build path. +CMakeLists.txt text eol=lf +*.cmake text eol=lf + +# Shared CI and package metadata should also stay LF. +*.yml text eol=lf +*.yaml text eol=lf +*.toml text eol=lf diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e60c0085..b56a9bb6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,4 +14,9 @@ ## File Management - Store temporary files, development documents, and drafts in `dev-docs/` - `.md` files in `docs/` must be English and require review before publishing -- Treat `dev-docs/` as `/tmp` \ No newline at end of file +- Treat `dev-docs/` as `/tmp` + +## Skills + +- **Submit PR:** Follow `.github/skills/pr-submit/SKILL.md` to create or update pull requests +- **Review PR:** Follow `.github/skills/pr-review/SKILL.md` to review pull requests \ No newline at end of file diff --git a/.github/skills/pr-review/SKILL.md b/.github/skills/pr-review/SKILL.md new file mode 100644 index 00000000..c60abd7d --- /dev/null +++ b/.github/skills/pr-review/SKILL.md @@ -0,0 +1,106 @@ +--- +name: pr-review +description: Address review comments and CI failures for the current branch's PR +--- + +## Constraints + +- Prepend `GH_PAGER=` to every `gh` command (bash/zsh), or set `$env:GH_PAGER=""` in PowerShell — never modify global config +- Key commands: `gh pr list --head ` · `gh pr view --comments` · `gh pr checks ` · `gh run view --log-failed` +- Workflow fixes **cannot be verified locally** — the fix is only confirmed once the remote CI re-runs and passes + +### Resolving Review Threads + +After fixing a P1/P2 comment, mark its thread as resolved on GitHub using the GraphQL mutation: + +```bash +GH_PAGER= gh api graphql -f query='mutation { resolveReviewThread(input: { threadId: "PRRT_xxxx" }) { thread { isResolved } } }' +``` + +To get thread IDs with their status and a brief description: + +```bash +GH_PAGER= gh api graphql -f query=' +{ + repository(owner: "OWNER", name: "REPO") { + pullRequest(number: PR_NUMBER) { + reviewThreads(first: 50) { + nodes { + id + isResolved + isOutdated + path + line + comments(first: 1) { nodes { body } } + } + } + } + } +}' | python3 -c " +import json,sys +data = json.load(sys.stdin) +for t in data['data']['repository']['pullRequest']['reviewThreads']['nodes']: + body = t['comments']['nodes'][0]['body'][:100].replace('\n',' ') if t['comments']['nodes'] else '' + print(f\"ID={t['id']}, resolved={t['isResolved']}, outdated={t['isOutdated']}, {t['path']}:{t['line']}\") + print(f\" >> {body}\") +" +``` + +Resolve threads that are **outdated** after re-checking whether the concern is already addressed or clearly superseded by the new code. GitHub can mark a thread outdated because the diff moved, not only because the issue was fixed. + +### Review Restraint Policy + +Before acting on any review comment or suggestion, classify it by importance: + +| Level | Criteria | Action | +|-------|----------|--------| +| **P1 — Must Fix** | Bug, security issue, broken behavior, API contract violation, CI failure | Fix immediately | +| **P2 — Should Fix** | Correctness risk, meaningful maintainability improvement, clear code smell with real impact | Fix with brief justification | +| **P3 — Conditional** | Style preference, minor naming, "could be cleaner" | Fix if it aligns with best practices **and** the change is safe + trivial; defer if complex or has any potential functional impact | +| **P4 — Reject** | Contradicts project conventions, introduces unnecessary complexity, or is factually wrong | Reject with explanation | + +**Rules:** +- For **P3**, apply this two-question test before touching the code: + 1. **Best practice?** — Does the change follow language/framework conventions (e.g. prefer imports over FQNs, use existing imports, standard patterns)? + 2. **Safe & trivial?** — Is the diff mechanical with zero risk of behavioral change and low effort? + - Both **yes** → fix it silently, mark thread resolved. + - Either **no** → do NOT modify code; record in the summary table and let the user decide. +- **Do not add comments to code** unless the comment explains non-obvious logic that is truly necessary. Never add comments just to acknowledge a review suggestion was applied. +- When the two-question test is ambiguous, prefer deferring rather than guessing. + +## Procedure + +1. **Locate PR** — get PR number for current branch +2. **Fix CI failures** — for each failing check: + - Fetch logs: `gh run view --log-failed` + - **Workflow issue** (wrong config, missing step, bad path): + 1. Fix `.github/workflows/` directly + 2. Commit & push the workflow change + 3. Wait for the re-triggered run to complete: poll with `GH_PAGER= gh pr checks ` (or `GH_PAGER= gh run watch `) until the affected check finishes + 4. If it **passes** → continue to the next failing check + 5. If it **still fails** → fetch new logs (`gh run view --log-failed`) and repeat from step i + - **Code issue, non-breaking**: fix source code directly + - **Code issue, breaking change required**: stop and report to developer for a decision +3. **Address review comments** — apply the Review Restraint Policy to each comment: + - P1/P2: implement the fix, then **immediately resolve the thread** using the GraphQL mutation above + - Outdated threads: re-check whether the concern is already addressed or clearly superseded; only then resolve the thread + - P3: apply the two-question test — fix + resolve if both answers are yes; otherwise record in summary and defer to user + - P4: record rejection reason in summary +4. **Commit & push** — single commit covering all non-workflow fixes (workflow fixes are pushed incrementally during step 2) +5. **Final verification** — once all fixes are applied, confirm every check is green: `GH_PAGER= gh pr checks ` + +## Output + +- Per CI failure: root cause and resolution (or escalation reason) +- After all fixes: summary of check statuses confirming all green +- **Review summary table** — produced at the end of every review session: + +```markdown +| # | Source (comment / CI) | Issue description | Priority | Action taken | Reason if not fixed | +|---|-----------------------|-------------------|----------|--------------|---------------------| +| 1 | Reviewer @xxx | use import instead of FQN | P3 | Fixed | Best practice + trivial mechanical change | +| 2 | Reviewer @xxx | rename internal var foo→bar | P3 | Not fixed | Non-standard opinion, no best-practice backing — deferred to user | +| 3 | CI: lint | null-check missing | P1 | Fixed | — | +``` + +All P3/P4 items that were **not** fixed must appear in this table with a clear reason, so the user can make an informed decision. diff --git a/.github/skills/pr-submit/SKILL.md b/.github/skills/pr-submit/SKILL.md new file mode 100644 index 00000000..80f5df67 --- /dev/null +++ b/.github/skills/pr-submit/SKILL.md @@ -0,0 +1,30 @@ +--- +name: pr-submit +description: Create or update a GitHub Pull Request — use ONLY when explicitly asked to create/submit/open a PR +--- + +## When to use + +**Only** when the user explicitly asks to create, submit, or open a Pull Request. +Do NOT invoke this skill for plain commit or push requests. + +## Constraints + +- Prepend `GH_PAGER=` to every `gh` command (bash/zsh), or set `$env:GH_PAGER=""` in PowerShell — never modify global config + +## Procedure + +1. **Verify & clean** — ensure changes are complete, correct, and free of debug artifacts +2. **Detect default branch** — determine the repository default branch first, for example: + - `DEFAULT_BRANCH=$(GH_PAGER= gh repo view --json defaultBranchRef -q '.defaultBranchRef.name')` +2. **Commit & push**: + - If on `$DEFAULT_BRANCH`, create a new feature branch first (avoid name conflicts) + - Commit all changes and push to remote +3. **Create or update PR**: + - Check if current branch already has a PR (`gh pr list --head --base "$DEFAULT_BRANCH"`) + - **Exists**: update title/description to reflect new commits + - **None**: create PR against `$DEFAULT_BRANCH` with title/description derived from changes + +## Output + +- What changed, why, impact, and how to verify \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 73e7ff89..e0f08dca 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,7 +10,7 @@ "args": [ "-l", "-c", - "( if [[ $(pwd) =~ ^/mnt ]]; then cmd.exe /c \" ( if not exist build (mkdir build ) ) & cd build & cmake .. \"; else mkdir -p build/Debug && pushd build/Debug && cmake ../.. -DCMAKE_BUILD_TYPE=Debug && popd && mkdir -p build/Release && pushd build/Release && cmake ../.. -DCMAKE_BUILD_TYPE=Release; fi )" + "( if [[ $(pwd) =~ ^/mnt ]]; then powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"if (-not (Test-Path 'build')) { New-Item -ItemType Directory -Path 'build' | Out-Null }; Set-Location 'build'; cmake ..\"; else mkdir -p build/Debug && pushd build/Debug && cmake ../.. -DCMAKE_BUILD_TYPE=Debug && popd && mkdir -p build/Release && pushd build/Release && cmake ../.. -DCMAKE_BUILD_TYPE=Release; fi )" ], "options": { "cwd": "${workspaceFolder}" @@ -18,10 +18,13 @@ "group": "build", "problemMatcher": "$gcc", "windows": { - "command": "cmd.exe", + "command": "powershell", "args": [ - "/c", - "( if not exist build (mkdir build ) ) & cd build & cmake .. " + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "if (-not (Test-Path 'build')) { New-Item -ItemType Directory -Path 'build' | Out-Null }; Set-Location 'build'; cmake .." ], "options": { "cwd": "${workspaceFolder}" @@ -36,7 +39,7 @@ "args": [ "-l", "-c", - "( if [[ $(pwd) =~ ^/mnt ]]; then cmd.exe /c \" ( if not exist build_shared (mkdir build_shared ) ) & cd build_shared & cmake .. -DCCAP_BUILD_SHARED=ON \"; else mkdir -p build_shared/Debug && pushd build_shared/Debug && cmake ../.. -DCMAKE_BUILD_TYPE=Debug -DCCAP_BUILD_SHARED=ON && popd && mkdir -p build_shared/Release && pushd build_shared/Release && cmake ../.. -DCMAKE_BUILD_TYPE=Release -DCCAP_BUILD_SHARED=ON; fi )" + "( if [[ $(pwd) =~ ^/mnt ]]; then powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"if (-not (Test-Path 'build_shared')) { New-Item -ItemType Directory -Path 'build_shared' | Out-Null }; Set-Location 'build_shared'; cmake .. -DCCAP_BUILD_SHARED=ON\"; else mkdir -p build_shared/Debug && pushd build_shared/Debug && cmake ../.. -DCMAKE_BUILD_TYPE=Debug -DCCAP_BUILD_SHARED=ON && popd && mkdir -p build_shared/Release && pushd build_shared/Release && cmake ../.. -DCMAKE_BUILD_TYPE=Release -DCCAP_BUILD_SHARED=ON; fi )" ], "options": { "cwd": "${workspaceFolder}" @@ -44,10 +47,13 @@ "group": "build", "problemMatcher": "$gcc", "windows": { - "command": "cmd.exe", + "command": "powershell", "args": [ - "/c", - "( if not exist build_shared (mkdir build_shared ) ) & cd build_shared & cmake .. -DCCAP_BUILD_SHARED=ON " + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "if (-not (Test-Path 'build_shared')) { New-Item -ItemType Directory -Path 'build_shared' | Out-Null }; Set-Location 'build_shared'; cmake .. -DCCAP_BUILD_SHARED=ON" ], "options": { "cwd": "${workspaceFolder}" @@ -70,10 +76,13 @@ "group": "build", "problemMatcher": "$gcc", "windows": { - "command": "cmd", + "command": "powershell", "args": [ - "/c", - "(git clean -fdx build || rmdir /s /q build) & (git clean -fdx build_shared || rmdir /s /q build_shared)" + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "git clean -fdx build; if ($LASTEXITCODE -ne 0 -and (Test-Path 'build')) { Remove-Item -Recurse -Force 'build' }; git clean -fdx build_shared; if ($LASTEXITCODE -ne 0 -and (Test-Path 'build_shared')) { Remove-Item -Recurse -Force 'build_shared' }; git clean -fdx examples; if ($LASTEXITCODE -ne 0 -and (Test-Path 'examples')) { Remove-Item -Recurse -Force 'examples' }" ], "options": { "cwd": "${workspaceFolder}" @@ -156,8 +165,14 @@ "group": "build", "problemMatcher": [], "windows": { - "command": "cmd", - "args": ["/c", "if not exist dev.cmake (copy cmake\\dev.cmake.example dev.cmake && echo dev.cmake created from template) else (echo dev.cmake already exists)"] + "command": "powershell", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "if (-not (Test-Path 'dev.cmake')) { Copy-Item 'cmake/dev.cmake.example' 'dev.cmake'; Write-Output 'dev.cmake created from template' } else { Write-Output 'dev.cmake already exists' }" + ] } }, { @@ -174,8 +189,14 @@ "group": "build", "problemMatcher": [], "windows": { - "command": "cmd", - "args": ["/c", "if exist dev.cmake del dev.cmake"] + "command": "powershell", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "if (Test-Path 'dev.cmake') { Remove-Item 'dev.cmake' -Force }" + ] } }, { @@ -213,7 +234,7 @@ "args": [ "-l", "-c", - "( if [[ $(pwd) =~ ^/mnt ]]; then cmd.exe /c \" cmake --build build --config Debug --parallel %NUMBER_OF_PROCESSORS% \"; else cmake --build build/Debug --config Debug --parallel $(nproc); fi )" + "( if [[ $(pwd) =~ ^/mnt ]]; then powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'cmake --build build --config Debug --parallel $env:NUMBER_OF_PROCESSORS'; else cmake --build build/Debug --config Debug --parallel $(nproc); fi )" ], "options": { "cwd": "${workspaceFolder}" @@ -221,10 +242,13 @@ "group": "build", "problemMatcher": "$gcc", "windows": { - "command": "cmd", + "command": "powershell", "args": [ - "/c", - "(if not exist build (mkdir build && cd build && cmake ..) ) & cmake --build build --config Debug --parallel %NUMBER_OF_PROCESSORS%" + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "if (-not (Test-Path 'build')) { New-Item -ItemType Directory -Path 'build' | Out-Null; Push-Location 'build'; cmake ..; Pop-Location }; cmake --build build --config Debug --parallel $env:NUMBER_OF_PROCESSORS" ], "options": { "cwd": "${workspaceFolder}" @@ -239,7 +263,7 @@ "args": [ "-l", "-c", - "( if [[ $(pwd) =~ ^/mnt ]]; then cmd.exe /c \" cmake --build build --config Release --parallel %NUMBER_OF_PROCESSORS% \"; else cmake --build build/Release --config Release --parallel $(nproc); fi )" + "( if [[ $(pwd) =~ ^/mnt ]]; then powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'cmake --build build --config Release --parallel $env:NUMBER_OF_PROCESSORS'; else cmake --build build/Release --config Release --parallel $(nproc); fi )" ], "options": { "cwd": "${workspaceFolder}" @@ -247,10 +271,13 @@ "group": "build", "problemMatcher": "$gcc", "windows": { - "command": "cmd", + "command": "powershell", "args": [ - "/c", - "(if not exist build (mkdir build && cd build && cmake ..) ) & cmake --build build --config Release --parallel %NUMBER_OF_PROCESSORS%" + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "if (-not (Test-Path 'build')) { New-Item -ItemType Directory -Path 'build' | Out-Null; Push-Location 'build'; cmake ..; Pop-Location }; cmake --build build --config Release --parallel $env:NUMBER_OF_PROCESSORS" ], "options": { "cwd": "${workspaceFolder}" @@ -265,7 +292,7 @@ "args": [ "-l", "-c", - "( if [[ $(pwd) =~ ^/mnt ]]; then cmd.exe /c \" cmake --build build_shared --config Debug --parallel %NUMBER_OF_PROCESSORS% \"; else cmake --build build_shared/Debug --config Debug --parallel $(nproc); fi )" + "( if [[ $(pwd) =~ ^/mnt ]]; then powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'cmake --build build_shared --config Debug --parallel $env:NUMBER_OF_PROCESSORS'; else cmake --build build_shared/Debug --config Debug --parallel $(nproc); fi )" ], "options": { "cwd": "${workspaceFolder}" @@ -273,10 +300,13 @@ "group": "build", "problemMatcher": "$gcc", "windows": { - "command": "cmd", + "command": "powershell", "args": [ - "/c", - "(if not exist build_shared (mkdir build_shared && cd build_shared && cmake .. -DCCAP_BUILD_SHARED=ON) ) & cmake --build build_shared --config Debug --parallel %NUMBER_OF_PROCESSORS%" + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "if (-not (Test-Path 'build_shared')) { New-Item -ItemType Directory -Path 'build_shared' | Out-Null; Push-Location 'build_shared'; cmake .. -DCCAP_BUILD_SHARED=ON; Pop-Location }; cmake --build build_shared --config Debug --parallel $env:NUMBER_OF_PROCESSORS" ], "options": { "cwd": "${workspaceFolder}" @@ -291,7 +321,7 @@ "args": [ "-l", "-c", - "( if [[ $(pwd) =~ ^/mnt ]]; then cmd.exe /c \" cmake --build build_shared --config Release --parallel %NUMBER_OF_PROCESSORS% \"; else cmake --build build_shared/Release --config Release --parallel $(nproc); fi )" + "( if [[ $(pwd) =~ ^/mnt ]]; then powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'cmake --build build_shared --config Release --parallel $env:NUMBER_OF_PROCESSORS'; else cmake --build build_shared/Release --config Release --parallel $(nproc); fi )" ], "options": { "cwd": "${workspaceFolder}" @@ -299,10 +329,13 @@ "group": "build", "problemMatcher": "$gcc", "windows": { - "command": "cmd", + "command": "powershell", "args": [ - "/c", - "(if not exist build_shared (mkdir build_shared && cd build_shared && cmake .. -DCCAP_BUILD_SHARED=ON) ) & cmake --build build_shared --config Release --parallel %NUMBER_OF_PROCESSORS%" + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "if (-not (Test-Path 'build_shared')) { New-Item -ItemType Directory -Path 'build_shared' | Out-Null; Push-Location 'build_shared'; cmake .. -DCCAP_BUILD_SHARED=ON; Pop-Location }; cmake --build build_shared --config Release --parallel $env:NUMBER_OF_PROCESSORS" ], "options": { "cwd": "${workspaceFolder}" diff --git a/docs/index.html b/docs/index.html index 4d9f1eb3..264355ed 100644 --- a/docs/index.html +++ b/docs/index.html @@ -46,8 +46,8 @@

(C)amera(CAP)ture

-

A lightweight, high-performance cross-platform camera capture library with video file playback support (Windows/macOS), plus Rust bindings.

-

轻量级、高性能的跨平台相机捕获库,支持视频文件播放(Windows/macOS),并提供 Rust bindings。

+

A lightweight, high-performance cross-platform camera capture library with video file playback support, plus Rust bindings.

+

轻量级、高性能的跨平台相机捕获库,支持视频文件播放,并提供 Rust bindings。

Hardware-accelerated conversion with AVX2, Apple Accelerate, NEON

支持 AVX2、Apple Accelerate、NEON 硬件加速

diff --git a/docs/js/main.js b/docs/js/main.js index 950d861c..43bc7552 100644 --- a/docs/js/main.js +++ b/docs/js/main.js @@ -162,7 +162,7 @@ assets.forEach(function(asset) { var name = (asset.name || '').toLowerCase(); var matched = false; - if (/\b(windows|win32|win64)\b/.test(name)) { + if (/\b(windows|win32|win64|msvc)\b/.test(name)) { platforms.windows.push(asset); matched = true; } diff --git a/src/ccap_convert_neon.cpp b/src/ccap_convert_neon.cpp index 4f6a4954..d2b7d8f1 100644 --- a/src/ccap_convert_neon.cpp +++ b/src/ccap_convert_neon.cpp @@ -1088,14 +1088,14 @@ void _yuyvToRgba_neon_imp(const uint8_t* src, int srcStride, int c_u = (int)u - 128; int c_v = (int)v - 128; - // Convert using dynamic coefficients - int r0 = (cy * c_y0 + cr * c_v) >> 6; - int g0 = (cy * c_y0 - cgu * c_u - cgv * c_v) >> 6; - int b0 = (cy * c_y0 + cb * c_u) >> 6; - - int r1 = (cy * c_y1 + cr * c_v) >> 6; - int g1 = (cy * c_y1 - cgu * c_u - cgv * c_v) >> 6; - int b1 = (cy * c_y1 + cb * c_u) >> 6; + // Use the same ×64 coefficients as the SIMD path to keep reduced-precision behavior consistent. + int r0 = (cy * c_y0 + cr * c_v + 32) >> 6; + int g0 = (cy * c_y0 - cgu * c_u - cgv * c_v + 32) >> 6; + int b0 = (cy * c_y0 + cb * c_u + 32) >> 6; + + int r1 = (cy * c_y1 + cr * c_v + 32) >> 6; + int g1 = (cy * c_y1 - cgu * c_u - cgv * c_v + 32) >> 6; + int b1 = (cy * c_y1 + cb * c_u + 32) >> 6; // Clamp to [0, 255] r0 = r0 < 0 ? 0 : (r0 > 255 ? 255 : r0); diff --git a/tests/test_frame_conversions.cpp b/tests/test_frame_conversions.cpp index e4a6613e..3186420b 100644 --- a/tests/test_frame_conversions.cpp +++ b/tests/test_frame_conversions.cpp @@ -648,6 +648,24 @@ void expectPackedYUVFrameMatchesReference(PackedYUVFrameData& frameData, ccap::P expected.stride()); } +int packedYuvReferenceTolerance(ccap::ConvertBackend backend, int width) { + switch (backend) { + case ccap::ConvertBackend::NEON: + return 2; + case ccap::ConvertBackend::AVX2: + return width >= 16 ? 2 : 0; + default: + return 0; + } +} + +void expectChannelNearReference(uint8_t actual, int expected, int tolerance, const char* channel, int x, int y, + const std::string& backendName) { + EXPECT_NEAR(static_cast(actual), expected, tolerance) + << channel << " mismatch at (" << x << ", " << y << "), backend: " << backendName + << ", tolerance: " << tolerance; +} + } // anonymous namespace // ---- Frame-level YUYV/UYVY → RGB/BGR conversion tests ---- @@ -780,7 +798,8 @@ TEST_P(FrameYUVFlipTest, UYVY_To_BGRA32_WithVerticalFlip) { INSTANTIATE_BACKEND_TEST(FrameYUVFlipTest); // ---- Fixed-value packed YUV tests ---- -// These use PixelTestUtils::yuv2rgbReference so they do not depend on the packed conversion helpers. +// These validate against PixelTestUtils::yuv2rgbReference. +// Reduced-precision SIMD math is allowed a tiny tolerance where the active backend actually uses it. class FrameYUVReferenceValueTest : public BackendParameterizedTest { protected: @@ -789,17 +808,19 @@ class FrameYUVReferenceValueTest : public BackendParameterizedTest { } }; -TEST_P(FrameYUVReferenceValueTest, YUYV_SolidColor_To_BGR24_MatchesReferencePixelValues) { +TEST_P(FrameYUVReferenceValueTest, YUYV_SolidColor_To_BGR24_StaysWithinReferenceTolerance) { auto backend = GetParam(); const int width = 8; const int height = 8; + const std::string backendName = BackendTestManager::getBackendName(backend); + const int tolerance = packedYuvReferenceTolerance(backend, width); PackedYUVFrameData yuyv(8, 8, ccap::PixelFormat::YUYV); fillPackedYUVDataSolid(yuyv.buffer.data(), yuyv.frame->stride[0], yuyv.frame->pixelFormat, yuyv.frame->width, yuyv.frame->height, 96, 90, 180); bool success = ccap::inplaceConvertFrame(yuyv.frame.get(), ccap::PixelFormat::BGR24, false); - ASSERT_TRUE(success) << "YUYV solid-color conversion failed, backend: " << BackendTestManager::getBackendName(backend); + ASSERT_TRUE(success) << "YUYV solid-color conversion failed, backend: " << backendName; int r = 0; int g = 0; @@ -809,24 +830,56 @@ TEST_P(FrameYUVReferenceValueTest, YUYV_SolidColor_To_BGR24_MatchesReferencePixe for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int offset = y * yuyv.frame->stride[0] + x * 3; - EXPECT_EQ(yuyv.frame->data[0][offset + 0], b); - EXPECT_EQ(yuyv.frame->data[0][offset + 1], g); - EXPECT_EQ(yuyv.frame->data[0][offset + 2], r); + expectChannelNearReference(yuyv.frame->data[0][offset + 0], b, tolerance, "B", x, y, backendName); + expectChannelNearReference(yuyv.frame->data[0][offset + 1], g, tolerance, "G", x, y, backendName); + expectChannelNearReference(yuyv.frame->data[0][offset + 2], r, tolerance, "R", x, y, backendName); } } } -TEST_P(FrameYUVReferenceValueTest, UYVY_SolidColor_To_RGBA32_MatchesReferencePixelValues) { +TEST_P(FrameYUVReferenceValueTest, YUYV_SolidColor_To_RGBA32_StaysWithinReferenceTolerance) { auto backend = GetParam(); const int width = 8; const int height = 8; + const std::string backendName = BackendTestManager::getBackendName(backend); + const int tolerance = packedYuvReferenceTolerance(backend, width); + + PackedYUVFrameData yuyv(width, height, ccap::PixelFormat::YUYV); + fillPackedYUVDataSolid(yuyv.buffer.data(), yuyv.frame->stride[0], yuyv.frame->pixelFormat, yuyv.frame->width, yuyv.frame->height, 96, 90, + 180); + + bool success = ccap::inplaceConvertFrame(yuyv.frame.get(), ccap::PixelFormat::RGBA32, false); + ASSERT_TRUE(success) << "YUYV solid-color conversion failed, backend: " << backendName; + + int r = 0; + int g = 0; + int b = 0; + PixelTestUtils::yuv2rgbReference(96, 90, 180, r, g, b, false, false); + + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + int offset = y * yuyv.frame->stride[0] + x * 4; + expectChannelNearReference(yuyv.frame->data[0][offset + 0], r, tolerance, "R", x, y, backendName); + expectChannelNearReference(yuyv.frame->data[0][offset + 1], g, tolerance, "G", x, y, backendName); + expectChannelNearReference(yuyv.frame->data[0][offset + 2], b, tolerance, "B", x, y, backendName); + EXPECT_EQ(yuyv.frame->data[0][offset + 3], 0xFF); + } + } +} + +TEST_P(FrameYUVReferenceValueTest, UYVY_SolidColor_To_RGBA32_StaysWithinReferenceTolerance) { + auto backend = GetParam(); + const int width = 8; + const int height = 8; + const std::string backendName = BackendTestManager::getBackendName(backend); + const int tolerance = packedYuvReferenceTolerance(backend, width); PackedYUVFrameData uyvy(width, height, ccap::PixelFormat::UYVY); fillPackedYUVDataSolid(uyvy.buffer.data(), uyvy.frame->stride[0], uyvy.frame->pixelFormat, uyvy.frame->width, uyvy.frame->height, 180, 54, 200); bool success = ccap::inplaceConvertFrame(uyvy.frame.get(), ccap::PixelFormat::RGBA32, false); - ASSERT_TRUE(success) << "UYVY solid-color conversion failed, backend: " << BackendTestManager::getBackendName(backend); + ASSERT_TRUE(success) << "UYVY solid-color conversion failed, backend: " << backendName; int r = 0; int g = 0; @@ -836,12 +889,41 @@ TEST_P(FrameYUVReferenceValueTest, UYVY_SolidColor_To_RGBA32_MatchesReferencePix for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int offset = y * uyvy.frame->stride[0] + x * 4; - EXPECT_EQ(uyvy.frame->data[0][offset + 0], r); - EXPECT_EQ(uyvy.frame->data[0][offset + 1], g); - EXPECT_EQ(uyvy.frame->data[0][offset + 2], b); + expectChannelNearReference(uyvy.frame->data[0][offset + 0], r, tolerance, "R", x, y, backendName); + expectChannelNearReference(uyvy.frame->data[0][offset + 1], g, tolerance, "G", x, y, backendName); + expectChannelNearReference(uyvy.frame->data[0][offset + 2], b, tolerance, "B", x, y, backendName); EXPECT_EQ(uyvy.frame->data[0][offset + 3], 0xFF); } } } +TEST_P(FrameYUVReferenceValueTest, UYVY_SolidColor_To_BGR24_StaysWithinReferenceTolerance) { + auto backend = GetParam(); + const int width = 8; + const int height = 8; + const std::string backendName = BackendTestManager::getBackendName(backend); + const int tolerance = packedYuvReferenceTolerance(backend, width); + + PackedYUVFrameData uyvy(width, height, ccap::PixelFormat::UYVY); + fillPackedYUVDataSolid(uyvy.buffer.data(), uyvy.frame->stride[0], uyvy.frame->pixelFormat, uyvy.frame->width, uyvy.frame->height, 180, 54, + 200); + + bool success = ccap::inplaceConvertFrame(uyvy.frame.get(), ccap::PixelFormat::BGR24, false); + ASSERT_TRUE(success) << "UYVY solid-color conversion failed, backend: " << backendName; + + int r = 0; + int g = 0; + int b = 0; + PixelTestUtils::yuv2rgbReference(180, 54, 200, r, g, b, false, false); + + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + int offset = y * uyvy.frame->stride[0] + x * 3; + expectChannelNearReference(uyvy.frame->data[0][offset + 0], b, tolerance, "B", x, y, backendName); + expectChannelNearReference(uyvy.frame->data[0][offset + 1], g, tolerance, "G", x, y, backendName); + expectChannelNearReference(uyvy.frame->data[0][offset + 2], r, tolerance, "R", x, y, backendName); + } + } +} + INSTANTIATE_BACKEND_TEST(FrameYUVReferenceValueTest); \ No newline at end of file