| name | render-scale-gate |
| description | 브라우저 멀티 아바타(PixiJS 2D 버튜버) 렌더 성능 천장을 정직하게 측정한다 — "이 기기가 아바타 N개를 몇 fps로 그리나"(멀티유저 방 인원·저사양 클라 게이트). 단일 페이지 WebGL N캔버스 하니스로 N=1→천장 스윕, 4개 하드 게이트(실제 렌더러 신원=Metal/SwiftShader 판별·메시변형 실행 증명·실렌더 시각·바닥값 아닌 실fps)로 가짜 측정을 막고, vsync 상한 함정을 인지해 진짜 천장을 찾는다. 트리거 — "렌더 성능 측정", "동시 아바타 한계", "N인 방 가능한가", "6인 방 성능", "fps 천장", "성능 게이트", "기기 성능 테스트", "저사양 측정", "Acer 측정", "render scale", "몇 명까지 렌더". 결과는 results.json에 기기별(env) run으로 append. |
렌더 성능 천장 측정 게이트 (멀티 아바타)
"한 클라이언트가 아바타 N개를 몇 fps로 그리나"를 측정한다. 멀티유저 버튜버 방의 인원 상한과 저사양 PC 생존(방송 ≥30fps)을 가르는 H3 리스크 게이트. 5번의 에이전트 시행착오로 산 교훈을 박제했다 — 측정이 가짜면 "맥=2개 한계" 또는 "8개 거뜬 무한"처럼 둘 다 틀린 결론이 나온다.
성숙도(정직): 렌더 부하만 측정. 통합 실시간 루프(MediaPipe 트래킹 + LiveKit N스트림 + 렌더 동시)는 이 스킬 범위 밖(별도 게이트).
1. 하니스 (재사용 도구)
experiments/theater-platform-001/poc0_render_scale/single_page/scale_webgl.html
- 단일 페이지, 한 WebGL 컨텍스트에 N개 아바타를 그리드 타일로, 단일 rAF 루프가 전부 매 프레임 렌더 → 그 루프의 page fps 측정.
- PixiJS v8 WebGL(
player/src/core/draw_pixi.js) + 메시변형(render_mode:'mesh', deformedVertices) = 실제 플랫폼 렌더 경로.
- 합성 드라이버(깜빡임·입·머리 sway) 매 프레임. 캐릭터 무관(
player/api/project 리그 교체 가능).
- 실행:
cd .../poc0_render_scale && python3 -m http.server 9100 → http://localhost:9100/single_page/scale_webgl.html. 정적이라 Vercel 배포 가능(다른 기기엔 URL로 뿌림).
2. ★4개 하드 게이트 (못 넘으면 측정 무효 — 가짜 성공 차단)
| 게이트 | 검사 | 실패 신호 |
|---|
| G1 렌더러 신원 | 실제 PixiJS 캔버스의 UNMASKED_RENDERER_WEBGL 읽기 | "Canvas 2D"·"SwiftShader"·"Software" = 우리 경로 아님/소프트웨어. "Metal/Apple/GPU명" = OK |
| G2 변형 실행 | render_mode==='mesh' + 정점이 프레임 간 실제 변함(샘플 delta>0 로그) | 부품 위치만 그리고 변형 생략 = 가짜 가벼움 |
| G3 시각 실렌더 | N=1·4 스크린샷에 아바타 전신 + sway 움직임 | 빈화면·정수리띠·정지 |
| G4 실fps | 바닥 기본값(0.9fps/정확히 N×1000ms) 아닌 실수치 | 바닥값 고정 = 측정 계기 고장 |
검증은 실제 브라우저 캡처로(메인이 스크린샷 직접 확인 — 성역 [[visual-verify-real-render]]). 에이전트 위임 시 "G1~G4 통과 전 완료보고 금지"를 명시.
3. ★vsync 함정 — 60fps는 천장이 아니다
rAF는 vsync(보통 60Hz)에 묶여 fps가 60에서 캡되고 frame_time은 ~16.67ms로 읽힌다. **N=8이 60fps여도 한계가 아니라 "아직 한 프레임에 여유"**일 뿐. 진짜 천장(여유 소진점)을 찾으려면:
- N을 12·16·24·32로 더 밀어 60fps가 처음 깨지는 지점을 찾거나,
performance.now()로 deform+render 구간의 실제 작업시간을 직접 재 헤드룸을 본다(16.67ms 대비 몇 ms 쓰나).
제품 인원(예: 6)이 목표면 천장 정밀 탐색은 불필요 — "목표 N에서 ≥30fps 여유"만 확인하면 충분.
4. 측정 프로토콜 + 데이터 스키마
- N=[1,2,3,4,6,8,...] 각 ~10초 안정화 후 page_fps·frame_time·스크린샷.
- 결과는
results/results.json의 runs[]에 기기별 run으로 append(덮어쓰기 금지):
{"env":"<기기-브라우저 라벨>","gl_renderer":"<G1 실제 문자열>",
"representative_of_real_gpu":<bool>,"deform_verified":<bool>,
"architecture":"single-page-webgl-grid-tile","rig":"<id>",
"samples":[{"n":1,"page_fps":<>,"frame_time_ms":<>,"status":"PASS|WARN","screenshot":"results/wgl_n1.png"}],
"ceiling_n":<page_fps<30 첫 N, 안 깨지면 null+측정최대N 주석>}
- env 라벨은 기기·렌더경로를 구분:
agent-browser-mac-m5-webgl-single-page, acer-chrome-webgl 등. 같은 스키마라 기기 간 비교가 곧 데이터.
5. 가드레일 (박제된 교훈 — 재발 방지)
- iframe 금지: N개 아바타를 iframe 격리로 띄우면 N≥3에서 브라우저가 rAF를 throttle → 자가 fps가 0.9/1000ms 바닥값으로 거짓 붕괴(렌더는 정상). 단일 페이지 N캔버스가 정답(스로틀 원천 제거 + 실제 1탭 모델).
- Canvas2D·변형생략 = 가짜: 측정이 쉬워 보이면 의심. 우리 질문은 PixiJS WebGL+메시변형 부하지 부품 배치가 아니다. G1·G2가 이걸 막는다.
- agent-browser 헤드리스 ≠ 실제 GPU: 헤드리스는 SwiftShader(소프트웨어)일 수 있어 절대 fps 무의미. G1으로 판별, software면
representative_of_real_gpu:false 라벨. (단 맥에서 agent-browser가 Metal로 헤디드 구동되기도 함 — G1로 확인.)
- 진짜 게이트는 저사양: 고사양(M-시리즈)이 통과해도 H3의 본질은 "싸구려 PC가 끊기나". 목표 기기(Acer 등)에서 같은 URL 재측정이 실판정. 목표 ≥30fps.
- 측정 적중 ≠ 끝: 숫자보다 G1~G4 통과가 먼저. 통과 전 "됐다" 금지([[ssot-summary-overclaim]]).
- 인원 확장은 별개 축: 클라 렌더 너머는 AOI(보이는 N명만 렌더)·SFU 용량·비용이 가른다(기술이 아니라 비용이 천장). [[theater-platform-mobile-ip-constraints]]