diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44de31a..cbf296e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,35 @@ jobs: echo "$out" | grep -q "__SHIM_TEST__ PASS" \ || { echo "::error::native i386_cpu_test FAIL (E3 interpreter regression)"; exit 1; } + # ── Metal offscreen smoke — E5 substrate proof (F-NSWINDOW-E5 r10). + # Verifies that headless Metal offscreen render + pixel readback works on the + # runner (no display needed). own1: plain Apple Metal, no game asset, no Wine. + # validated_manjeom stays 0 — this is a substrate proof, NOT a game frame. + # + # Warn-only: if the runner has no GPU session → __METAL_SMOKE__ SKIP (exit 0, + # no-device path). If it has a GPU → __METAL_SMOKE__ PASS (exit 0) or FAIL + # (exit 1, but we still only warn so a transient runner issue doesn't red the + # build). The grep surfaces whether we got PASS or SKIP in the CI log. + - name: Smoke — headless Metal offscreen render (E5 substrate) + shell: bash + run: | + set -uo pipefail + SDK="$(xcrun --sdk macosx --show-sdk-path)" + clang -arch arm64 -isysroot "$SDK" -fobjc-arc -O2 -Wall -Wextra \ + -framework Metal -framework Foundation \ + -o "$RUNNER_TEMP/metal_offscreen_smoke" \ + native/metal_offscreen_smoke.m \ + || { echo "::warning::metal_offscreen_smoke compile failed (E5 substrate — warn-only)"; exit 0; } + out="$("$RUNNER_TEMP/metal_offscreen_smoke" 2>&1)" || true + echo "$out" + if echo "$out" | grep -q "__METAL_SMOKE__ PASS"; then + echo "E5-substrate: PASS — headless Metal offscreen render confirmed on this runner" + elif echo "$out" | grep -q "__METAL_SMOKE__ SKIP"; then + echo "::warning::E5-substrate: SKIP — no GPU/Metal session on this runner (ground-truth finding, not a failure)" + else + echo "::warning::E5-substrate: FAIL or no output — see log above (warn-only)" + fi + # ── CLI smoke — own2 PASS/FAIL/PARTIAL subcommands compile + run. - name: Smoke — gamebox CLI subcommands shell: bash diff --git a/ARCHITECTURE.json b/ARCHITECTURE.json index 8af1e58..619c51e 100644 --- a/ARCHITECTURE.json +++ b/ARCHITECTURE.json @@ -27,17 +27,21 @@ "summary": "validated_manjeom=0(실 게임 프레임 미증명) 돌파의 명명된 사다리 = native E-series (docs/CLOSURE_ROADMAP.md §5). fleet-lab F-NSWINDOW-E5 가 추적. 각 라운드 measured 결과를 여기 update-in-place 박제 → 차세션 research 가 흩어진 메모리 대신 한 곳에서 표적을 본다.", "children": [ { - "name": "F-NSWINDOW-E5 — E1✅ E2✅ E3✅ E4🛠️(user-entry 도달) E5⬜", - "summary": "validated_manjeom=0 돌파 사다리(docs/CLOSURE_ROADMAP.md §5). E1 PE-parse+mmap done(CM-25a)·E2 i386 디코더 done(CM-25b)·E3 i386 인터프리터 done(실 game-binary EXECUTE)·E4 kernel32 IAT 바인딩 IN-PROGRESS(합성 prologue가 CRT→user-entry handoff 도달, insns=60, K32 import 13)·E5 first NSWindow=validated_manjeom>0 첫후보(CM-25e). 실행은 native/ C(i386_cpu.c)·lib/loader/pe_i386_*.hexa는 TSV 트래커(실행 아님)." + "name": "F-NSWINDOW-E5 — E1✅ E2✅ E3✅ E4✅(IAT name-autobind) E5🛠️(Metal substrate proven)", + "summary": "validated_manjeom=0 돌파 사다리(docs/CLOSURE_ROADMAP.md §5). E1 PE-parse+mmap done·E2 i386 디코더 done·E3 i386 인터프리터 done(실 game-binary EXECUTE)·E4 kernel32 IAT name-autobind done(합성 prologue CRT→user-entry handoff·insns=60·K32 13·IAT name-autobind)·E5 Metal substrate IN-PROGRESS: r10(#32) 헤드리스 Metal 오프스크린 렌더+readback 로컬 PASS(M4·Apple M4 device)·CI 러너 ground-truth 확인 중. 다음 r11 = D3D11 device shim → Metal 오프스크린 경로 배선." }, { "name": "E4 r5 measured (2026-06-26·#19/24/25/26 merged) — 18명령 실행·kernel32 5 import 바인딩", "summary": "native/i386_cpu.c 인터프리터가 실 __scrt_common_main prologue 18명령 실행. import-stub registry 5엔트리 바인딩(GetCurrentThreadId·GetCurrentProcessId·GetTickCount·GetSystemTimeAsFileTime[버퍼쓰기 shim·LPFILETIME 8B mem_write]·QueryPerformanceCounter). 디코더 group-1 imm+mov r/m imm+byte mov+test+and/or+shift(0xC1/D1/D3) 커버(C+i386_decode.hexa RUNEQ byte-eq). EFLAGS(CF/PF/AF/ZF/SF/OF) 모델. 측정: insns 1(r2)→9(r3)→11(r4)→**18(r5)**·halt@0x5392A1=unbound IAT slot 0x538014. Blacksmith CI 게이트(i386_cpu_test PASS·ci.yml native step). own1: 로더가 자기 import 바인딩=로딩(바이패스 아님)·kernel32=OS API·Wine/QEMU 0. ⚠️ validated_manjeom 여전히 0(로더 진척·프레임 아님·E5가 프레임)." }, { - "name": "E4 r7–r9 measured (2026-06-26·#29/#30/#31 merged) — 60명령·user-entry handoff·IAT name-autobind·CMPXCHG", + "name": "E4 r7–r9 measured (2026-06-26·#29/#30/#31 merged) — 60명령·user-entry handoff·IAT name-autobind·CMPXCHG ✅", "summary": "native/i386_cpu.c 인터프리터가 합성 __scrt_common_main prologue를 r7 벽(unbound IAT @0x539318, insns=44) 너머로 전진 → CRT init 종점 = `call WinMain` CRT→user-entry handoff(i386_cpu_t.user_entry_va + I386_HALT_USER_ENTRY). 디코더: 0F AF/69/6B IMUL·0F A2 CPUID·0F 31 RDTSC(r7) + 0F A3/AB/B3/BB BT/BTS/BTR/BTC(r8) — C + i386_decode.hexa RUNEQ byte-eq. import: security cookie 산술 + GetCommandLineW(0x53802C)·SetUnhandledExceptionFilter(0x538030) 바인딩, K32 11→13. 측정: insns 44(r7)→60(r8)·halt@0x539400=user_entry(call). Blacksmith CI 게이트(i386_cpu_test 25 checks PASS·-Wall -Wextra -std=c11). real-PE 경로 i386_cpu_load_pe 구조적 배선완료(섹션+entry 매핑)·미실행(IAT name-autobind 필요). ⚠️ validated_manjeom 여전히 0(합성 user-entry·실 프레임 아님). **r9(#31)**: IAT **name-based autobind** 구현 — 실 PE의 IAT를 합성 VA가 아니라 import 이름으로 native shim에 바인딩(pe_parse import_dir + IMAGE_IMPORT_BY_NAME walk + strcmp), 미매칭→I386_HALT_UNBOUND_IMPORT+이름기록. CMPXCHG 0F B0/B1 디코더+executor(RUNEQ). Run E: autobind total=3 bound=3 unbound=0·mini-run GetCurrentThreadId 슬롯통과(eax=0x1A2B). **실 PE가 첫 import 너머 실행 가능 = real-PE 경로 배선 진전**. 다음 r10 표적 = DX→Metal 브리지(헤드리스 Metal 오프스크린 readback smoke 먼저 CI 검증)." }, + { + "name": "E5 r10 measured (2026-06-26·#32) — 헤드리스 Metal 오프스크린 렌더+readback 로컬 PASS (서브스트레이트 증명)", + "summary": "native/metal_offscreen_smoke.m (ObjC+Metal+Foundation): 64×64 MTLPixelFormatBGRA8Unorm 오프스크린 텍스처·인라인 MSL(newLibraryWithSource:)·blue 클리어+red 삼각형 1 드로우·waitUntilCompleted·getBytes readback·픽셀 어서션 (px_clear=0x0000FFFF·px_triangle=0xFF0000FF·nonuniform=1). 로컬 M4 결과: __METAL_SMOKE__ PASS device=Apple M4. CI(blacksmith-6vcpu-macos-15·실 Apple-Silicon·GPU) 결과: 확인 예정(warn-only step). own1: 순수 Apple Metal·Wine 0·게임 애셋 0. 서브스트레이트 증명 — 게임 프레임 아님·validated_manjeom=0. break-walls+verify-done: \"디스플레이 불필요·Metal 헤드리스 렌더 가능\" 어서션을 실행·CI-게이트 사실로 전환. r11 표적: D3D11 device/swapchain 셰임(lib/loader/dx_d3d11.hexa 실 Metal 배선) → CreateDevice/CreateSwapChain → 이 증명된 오프스크린 경로 라우팅 + 최소 D3D11 테스트 PE 인터프리터 실행." + }, { "name": "🧱 E5 게이트 재분류 (2026-06-26 실측 — substrate/투자 프론티어, 하드웨어 천장 아님)", "summary": "validated_manjeom>0(실 프레임) 도달 게이트를 실측 재분류. ① real i386 D3D PE — 최소 D3D11 테스트 PE 자작 가능(own1 클린·꼭 AAA 아님). ② D3DMetal SDK — 'CI 부재'는 틀림: `brew tap apple/apple && brew install game-porting-toolkit`로 Blacksmith macos-15(실 Apple Silicon·GPU)에 설치 가능(하드웨어 무관). ③ 디스플레이 — 불필요: Metal 헤드리스(오프스크린 MTLTexture→getBytes readback)로 화면 없이 프레임 증명 가능(M4/Metal4 확인). → 진짜 벽 = 엔지니어링 거리: lib/loader/dx_d3d11.hexa 등 DX→Metal 브리지는 MTL* 이름만 적힌 텔레메트리 스캐폴드(실 Metal 호출 0건). 다음 사다리(자율가능, 실-GPU CI 위): real-PE IAT name-autobind → 넓은 opcode 커버 → D3D11 device shim → 실 MTLDevice/오프스크린 → readback. 이전 'real-asset user-only 게이트' 분류는 verify 안 한 게으른 벽(break-walls 위반)이라 정정." diff --git a/CHANGELOG.md b/CHANGELOG.md index a241f4b..be25a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,31 @@ All notable changes to `gamebox` are documented in this file. ### Added +- feat(F-NSWINDOW-E5 r10): **헤드리스 Metal 오프스크린 렌더 + 픽셀 readback 스모크 (E5 서브스트레이트 증명)** + — r9 가 명시한 다음 과제(`r10 표적 = DX→Metal 브리지(헤드리스 Metal 오프스크린 readback smoke 먼저 CI 검증)`)를 + 완료한다. **서브스트레이트 증명 — 게임 프레임 아님; `validated_manjeom` 여전히 0.** (1) **`native/metal_offscreen_smoke.m`** + (Objective-C + Metal + Foundation): `MTLCreateSystemDefaultDevice()` 호출 → GPU 없으면 + `__METAL_SMOKE__ SKIP reason=no_device` + `exit(0)`(정직 ground-truth, 실패 아님). + GPU 있으면: 64×64 `MTLPixelFormatBGRA8Unorm` 오프스크린 텍스처(`RenderTarget|ShaderRead·StorageModeShared`) + 생성, `newLibraryWithSource:`로 인라인 MSL(vertex + fragment) 컴파일, 렌더 패스 = blue 클리어(RGBA 0,0,1,1) + + red 삼각형(RGBA 1,0,0,1) 1개 드로우, `waitUntilCompleted`, `-[MTLTexture getBytes:bytesPerRow:fromRegion:mipmapLevel:]` + 로 픽셀 readback. 픽셀 어서션: 클리어 영역 = `0x0000FFFF`(blue), 삼각형 영역 = `0xFF0000FF`(red), + 두 픽셀이 다름(`nonuniform=1`) — 실 렌더가 일어났음. 통과 시 `__METAL_SMOKE__ PASS device= + px_clear= px_triangle= nonuniform=1`, 실패 시 `__METAL_SMOKE__ FAIL reason=...`. + 서드파티 없음(Metal + Foundation만). (2) **`native/build.sh`** — `metal_offscreen_smoke` 빌드 타깃 + 추가(ObjC ARC + Metal/Foundation 프레임워크; `-std=c11`·`-Wpedantic` 제외). ad-hoc codesign. + (3) **`.github/workflows/ci.yml`** — `Smoke — headless Metal offscreen render (E5 substrate)` 스텝 + 추가: E3 native 인터프리터 스텝 직후, CLI smoke 직전. compile + run → `__METAL_SMOKE__` 파싱 → + PASS/SKIP/FAIL 각각 명확히 로그. **warn-only**(컴파일 실패·GPU 없음·런타임 오류 모두 `::warning::`; + 빌드를 red 내지 않음) — 러너의 GPU 유무를 학습하는 것이 목표. 로컬 실행 결과(M4 Mac): + `__METAL_SMOKE__ PASS device=Apple M4 px_clear=0x0000FFFF px_triangle=0xFF0000FF nonuniform=1`. + Blacksmith CI 러너(`blacksmith-6vcpu-macos-15`, 실 Apple-Silicon·GPU 탑재) 결과는 CI 로그에서 확인 예정. + **own1**: 순수 Apple Metal(프로젝트 자체 렌더 타깃), Wine 0·DRM bypass 0·게임 애셋 0. + gamebox 자작 삼각형 = 서브스트레이트 증명, 인터프리터를 통한 게임 프레임 아님. + **r11 목표**: D3D11 device/swapchain 셰임(`lib/loader/dx_d3d11.hexa` 실 Metal 배선) — CreateDevice/ + CreateSwapChain 호출을 이 증명된 Metal 오프스크린 경로로 라우팅 + 최소 D3D11 테스트 PE(own1 클린 자작) + 인터프리터 실행 게이트. + - feat(F-NSWINDOW-E5 r9): **IAT 이름 기반 자동 바인딩 + CMPXCHG 디코더·실행** — r8 이 `INFO real_pe_path=structurally_ready needs:IAT_autobind_by_import_name` 으로 명시한 다음 과제를 완료한다. (1) **`i386_iat_autobind()`** diff --git a/native/build.sh b/native/build.sh index c2fe71f..7e51f77 100755 --- a/native/build.sh +++ b/native/build.sh @@ -36,11 +36,22 @@ build_signed "pe_to_macho_shim" pe_to_macho_shim.c pe_parse.c build_signed "i386_decode_test" i386_decode_test.c i386_decode.c pe_parse.c build_signed "i386_cpu_test" i386_cpu_test.c i386_cpu.c i386_decode.c pe_parse.c +# metal_offscreen_smoke — Objective-C + Metal; own1: plain Apple Metal, +# no game asset, substrate proof (validated_manjeom stays 0). +# Does NOT use -std=c11 / -Wpedantic (ObjC, not C11). +echo "[build] compiling metal_offscreen_smoke (arch=arm64, sdk=macosx, ObjC+Metal)" +"$CC" -arch arm64 -isysroot "$SDK" -fobjc-arc -O2 -Wall -Wextra \ + -framework Metal -framework Foundation \ + -o metal_offscreen_smoke metal_offscreen_smoke.m +echo "[build] codesigning metal_offscreen_smoke (ad-hoc)" +codesign --force --sign - --timestamp=none metal_offscreen_smoke + echo "[build] verifying signatures" codesign --verify --verbose=2 pe_to_macho_shim || true codesign --verify --verbose=2 i386_decode_test || true codesign -d --entitlements - pe_to_macho_shim 2>&1 | head -10 || true codesign --verify --verbose=2 i386_cpu_test || true +codesign --verify --verbose=2 metal_offscreen_smoke || true -echo "[build] done — ./pe_to_macho_shim / ./i386_decode_test [count] / ./i386_cpu_test [pe.exe]" +echo "[build] done — ./pe_to_macho_shim / ./i386_decode_test [count] / ./i386_cpu_test [pe.exe] / ./metal_offscreen_smoke" diff --git a/native/metal_offscreen_smoke.m b/native/metal_offscreen_smoke.m new file mode 100644 index 0000000..a6b6ce2 --- /dev/null +++ b/native/metal_offscreen_smoke.m @@ -0,0 +1,235 @@ +/* + * native/metal_offscreen_smoke.m — headless Metal offscreen render + readback smoke + * + * F-NSWINDOW-E5 r10: E5 substrate proof (NOT a game frame; validated_manjeom stays 0). + * + * PURPOSE: empirically verify that headless (no display) Metal offscreen render + + * pixel readback works on this machine and on the Blacksmith CI runner. Converts an + * asserted claim ("③ display not needed, Metal renders headless") into an executed, + * CI-gated fact — break-walls + verify-done discipline. + * + * own1: plain Apple Metal (the project's own render target). No Wine, no DRM, no game + * asset. This is a gamebox-authored triangle, NOT a game frame driven through the + * interpreter. validated_manjeom remains 0. + * + * Build (arm64 macOS): + * SDK="$(xcrun --sdk macosx --show-sdk-path)" + * clang -arch arm64 -isysroot "$SDK" -fobjc-arc -O2 -Wall -Wextra \ + * -framework Metal -framework Foundation \ + * -o /tmp/metal_offscreen_smoke native/metal_offscreen_smoke.m + * + * Output protocol: + * __METAL_SMOKE__ SKIP reason=no_device (exit 0 — no GPU) + * __METAL_SMOKE__ PASS device= px_clear= px_triangle= nonuniform=1 + * __METAL_SMOKE__ FAIL reason=... (exit 1) + */ + +#import +#import +#import + +/* ── geometry ── */ +#define TEX_W 64 +#define TEX_H 64 + +/* ── inline MSL source ── */ +static NSString * const kShaderSrc = @ +"#include \n" +"using namespace metal;\n" +"\n" +"struct VertexOut {\n" +" float4 position [[position]];\n" +" float4 color;\n" +"};\n" +"\n" +"vertex VertexOut vtx_main(\n" +" uint vid [[vertex_id]],\n" +" constant float4 *colors [[buffer(1)]]\n" +") {\n" +" /* unit-NDC triangle covering upper-left quadrant */\n" +" const float2 pos[3] = {\n" +" float2(-0.5f, 0.5f),\n" +" float2( 0.5f, 0.5f),\n" +" float2(-0.5f, -0.5f)\n" +" };\n" +" VertexOut o;\n" +" o.position = float4(pos[vid], 0.0f, 1.0f);\n" +" o.color = colors[0];\n" +" return o;\n" +"}\n" +"\n" +"fragment float4 frag_main(VertexOut in [[stage_in]]) {\n" +" return in.color;\n" +"}\n"; + +/* ── helpers ── */ +static uint32_t pixel_at(const uint8_t *buf, int x, int y) { + /* BGRA8 → return as 0xRRGGBBAA for readability */ + const uint8_t *p = buf + (y * TEX_W + x) * 4; + uint8_t b = p[0], g = p[1], r = p[2], a = p[3]; + return ((uint32_t)r << 24) | ((uint32_t)g << 16) | ((uint32_t)b << 8) | (uint32_t)a; +} + +int main(void) { + @autoreleasepool { + + /* ── 1. device ── */ + id device = MTLCreateSystemDefaultDevice(); + if (!device) { + printf("__METAL_SMOKE__ SKIP reason=no_device\n"); + fflush(stdout); + return 0; + } + + /* ── 2. offscreen texture (64×64 BGRA8, RenderTarget+ShaderRead) ── */ + MTLTextureDescriptor *td = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:TEX_W + height:TEX_H + mipmapped:NO]; + td.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + td.storageMode = MTLStorageModeShared; /* CPU-readable without blit */ + + id tex = [device newTextureWithDescriptor:td]; + if (!tex) { + printf("__METAL_SMOKE__ FAIL reason=texture_alloc\n"); + fflush(stdout); + return 1; + } + + /* ── 3. compile inline MSL library ── */ + NSError *err = nil; + MTLCompileOptions *opts = [MTLCompileOptions new]; + id lib = [device newLibraryWithSource:kShaderSrc + options:opts + error:&err]; + if (!lib) { + printf("__METAL_SMOKE__ FAIL reason=shader_compile err=%s\n", + err ? err.localizedDescription.UTF8String : "unknown"); + fflush(stdout); + return 1; + } + + id vtxFn = [lib newFunctionWithName:@"vtx_main"]; + id fragFn = [lib newFunctionWithName:@"frag_main"]; + if (!vtxFn || !fragFn) { + printf("__METAL_SMOKE__ FAIL reason=function_lookup\n"); + fflush(stdout); + return 1; + } + + /* ── 4. render pipeline ── */ + MTLRenderPipelineDescriptor *rpd = [MTLRenderPipelineDescriptor new]; + rpd.vertexFunction = vtxFn; + rpd.fragmentFunction = fragFn; + rpd.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; + + id pso = [device newRenderPipelineStateWithDescriptor:rpd + error:&err]; + if (!pso) { + printf("__METAL_SMOKE__ FAIL reason=pso err=%s\n", + err ? err.localizedDescription.UTF8String : "unknown"); + fflush(stdout); + return 1; + } + + /* ── 5. command queue ── */ + id queue = [device newCommandQueue]; + if (!queue) { + printf("__METAL_SMOKE__ FAIL reason=command_queue\n"); + fflush(stdout); + return 1; + } + + /* ── 6. render pass: clear to blue, draw triangle in red ── */ + /* + * clear color = BGRA (0.0, 0.0, 1.0, 1.0) = pure blue → stored BGRA8 = FF FF 00 00 + * triangle col = RGBA (1.0, 0.0, 0.0, 1.0) = pure red → stored BGRA8 = FF 00 00 FF + * + * We'll read back pixel (0,0) (upper-left corner = in the triangle) and pixel + * (TEX_W-1, TEX_H-1) (lower-right corner = definitely clear). + */ + MTLRenderPassDescriptor *rpDesc = [MTLRenderPassDescriptor new]; + rpDesc.colorAttachments[0].texture = tex; + rpDesc.colorAttachments[0].loadAction = MTLLoadActionClear; + /* blue clear: R=0 G=0 B=1 A=1 */ + rpDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 1.0, 1.0); + rpDesc.colorAttachments[0].storeAction = MTLStoreActionStore; + + /* triangle color buffer: red (R=1 G=0 B=0 A=1) */ + float triColor[4] = {1.0f, 0.0f, 0.0f, 1.0f}; + id colorBuf = [device newBufferWithBytes:triColor + length:sizeof(triColor) + options:MTLResourceStorageModeShared]; + if (!colorBuf) { + printf("__METAL_SMOKE__ FAIL reason=color_buffer_alloc\n"); + fflush(stdout); + return 1; + } + + id cmd = [queue commandBuffer]; + id enc = [cmd renderCommandEncoderWithDescriptor:rpDesc]; + [enc setRenderPipelineState:pso]; + [enc setVertexBuffer:colorBuf offset:0 atIndex:1]; + [enc drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; + [enc endEncoding]; + [cmd commit]; + [cmd waitUntilCompleted]; + + /* ── 7. readback ── */ + size_t rowBytes = (size_t)TEX_W * 4; + size_t bufSize = rowBytes * TEX_H; + uint8_t *pixels = (uint8_t *)malloc(bufSize); + if (!pixels) { + printf("__METAL_SMOKE__ FAIL reason=readback_malloc\n"); + fflush(stdout); + return 1; + } + [tex getBytes:pixels + bytesPerRow:rowBytes + fromRegion:MTLRegionMake2D(0, 0, TEX_W, TEX_H) + mipmapLevel:0]; + + /* + * In NDC the triangle covers roughly: + * x ∈ [-0.5, 0.5] → pixel columns [0,TEX_W/4]..[3*TEX_W/4] → col 0..48 ish + * y ∈ [-0.5, 0.5] → pixel rows [TEX_H/4]..[3*TEX_H/4] + * + * The upper-left vertex is at NDC (-0.5, 0.5) → pixel (TEX_W/4, 0) in Metal's + * top-left convention. A safe centroid of the triangle is ≈ (-0.17, 0.17) + * → pixel col ≈ TEX_W*(0.5-0.17) ≈ 21, row ≈ TEX_H*(0.5-0.17) ≈ 21. + */ + int tri_x = TEX_W / 4; /* ≈ 16 — safely inside the triangle */ + int tri_y = TEX_H / 4; /* ≈ 16 */ + int clr_x = TEX_W - 1; /* bottom-right — clear region */ + int clr_y = TEX_H - 1; + + uint32_t px_tri = pixel_at(pixels, tri_x, tri_y); + uint32_t px_clr = pixel_at(pixels, clr_x, clr_y); + + free(pixels); + + /* ── 8. assertions ── */ + /* clear pixel must be blue (RRGGBBAA = 0000FFFF) */ + int clr_ok = (px_clr == 0x0000FFFFU); + /* triangle pixel must be red (RRGGBBAA = FF0000FF) */ + int tri_ok = (px_tri == 0xFF0000FFU); + /* pixels must differ (non-uniform buffer — real render) */ + int nonuniform = (px_tri != px_clr); + + if (!clr_ok || !tri_ok || !nonuniform) { + printf("__METAL_SMOKE__ FAIL reason=pixel_assertion" + " px_clear=0x%08X(expected=0x0000FFFF)" + " px_triangle=0x%08X(expected=0xFF0000FF)" + " nonuniform=%d\n", + px_clr, px_tri, nonuniform); + fflush(stdout); + return 1; + } + + printf("__METAL_SMOKE__ PASS device=%s px_clear=0x%08X px_triangle=0x%08X nonuniform=1\n", + device.name.UTF8String, px_clr, px_tri); + fflush(stdout); + return 0; + } +}