# 지속 프레임 비용·투과 패스 줄이기 설계 (성능 하위 프로젝트 C) 작성일: 2026-10-29 상태: 구현 완료(2026-10-29, 로컬 main 머지 대기). 측정 결과는 계획 문서의 측정 기록 ## 1. 목적과 확정 요구 순간 끊김을 다룬 A(셰이더 재컴파일)·B(절단 준비 CPU·판 전환)와 달리, 매 프레임 꾸준히 드는 비용을 줄여 평소 프레임을 안정적으로 만든다. 성능 개선 3개 하위 프로젝트 중 마지막이다. A·B는 main에 반영되었고 미배포다. 사용자가 확정한 내용: - 대상은 다섯 항목이다: (1) DOM 레이아웃 읽기, (2) 천천히 썰기 매 프레임 할당, (3) 메인 화면 투과 패스, (4) 천천히 썰기 등급 하향 재컴파일, (5) 메스 칼자국 재생성 할당. - **실기기 전·후 측정을 완료 기준에 포함한다.** 기기·조건은 B와 같다(Galaxy S22 Ultra 앱, medium, 앱 강제 종료 후 처음 실행, 기준·변경 APK 번갈아 3회, 중앙값). 지표는 **정해진 시간 동안의 평균·p95 프레임 간격**이다. 절대 ms 예산은 두지 않는다. - 측정 장면은 메인 화면 대기, 천천히 썰기 중, 메스 긋는 중, 결과 카드 등장이다. 등급 하향(4)은 그 순간의 최대 간격·그리기 안 막힘도 본다. - 0단계에서 기준선을 먼저 재고, 끊김이 작게 나와도 개선 여지가 있으면 포함한다(B와 같다). - 접근 1(항목별 맞춤 수정, 순서와 강도는 0단계 측정으로 결정)을 택했다(3절). - 작업 브랜치에서 구현하고 **로컬 main 머지까지만** 한다. 푸시하지 않는다. - 투과 패스를 끄면서 생길 수 있는 **천천히 썰기 홈 판의 PLAY 첫 프레임 재컴파일도 막는다**: 메인 화면 진입 뒤 투과 패스를 한 번 그려 렌더 타깃용 셰이더 변형을 데운 뒤 끈다. 측정 장면에 "천천히 썰기 홈 판에서 PLAY"를 더한다(4.3). - 등급 하향(4)의 범위는 **천천히 썰기 판 하향 순간의 모든 그리기 안 컴파일**이다(유령 칼날 같은 low 전용 무대 자원 포함). 기준선에서 막힘이 이미 0이면 "해당 없음"으로 기록하고 뺀다(4.4). - 프레임 간격(평균·p95)과 함께 **게임 루프 메인 스레드 작업 시간(평균·p95)**을 보조 지표로 잰다. 화면 주사율에 묶인 장면은 간격이 변하지 않을 수 있기 때문이다(6.2). 설계자 판단(사용자 확인 대상, B와 같은 원칙): - **외관·판정이 바뀌지 않아야 한다.** 계산 방식·시점·셰이더 변형 선택만 바꾼다. 투과 변형 전환(4.3)은 자동 화소 비교로 "같음"을 확인한다(5절). 화소 비교가 판정선을 넘으면 멈추고 시각 검수 방식을 사용자에게 묻는다. - 기존 에뮬레이션 성능 예산과 품질 임계값은 완화하지 않는다. `FpsGovernor` 정책도 바꾸지 않는다. - iPhone 측정은 범위에서 빼고 미완료로 남긴다. ## 2. 현재 구조와 원인 | 항목 | 위치 | 현재 동작 | |---|---|---| | DOM 레이아웃 읽기 | `src/core/Game.ts` `fitCamera`(HUD `.hud-top`·`.knife-dock`·`.hud-hint`, 메뉴 `.menu-title`·`.menu-card`, 캔버스의 `getBoundingClientRect`), `jellyToScreen`·`plateToScreen`·`render.canvasRect()`(캔버스) | `fitCamera`는 결과 카드·메뉴 오버레이가 움직이는 동안(`overlayCur`가 목표를 따라가는 프레임) 매 프레임 불린다. 튜토리얼 손가락(`KnifeMode`), 휘둘러 베기 몸체 화면 좌표(`SwipeMode`), 천천히 썰기(`SlowMode`)도 매 프레임 캔버스 위치를 읽는다. 같은 프레임에 HUD 문구·클래스를 바꾼 뒤 읽으면 브라우저가 강제 동기 레이아웃을 한다 | | 천천히 썰기 할당 | `src/core/modes/SlowMode.ts` 프레임 경로(`beforeRender`, 미리보기·느낌·막대 갱신), `src/core/slow/` | 갱신마다 객체 펼침(`{ ...deform }`)·배열 생성 등이 있다. 후보 약 51곳 중 실제 매 프레임 경로는 0단계에서 가린다 | | 메인 화면 투과 패스 | `src/jelly/JellyMaterial.ts` `setSlowTransmission`(`SLOW_TRANS_MIN` 1e-3), `Game.frame`(매 프레임 `setSlowTransmission(mat, sceneK)`), 메인 화면은 `setScene(TABLE_VIEW, 0)`(`Game.ts` showMenu), PLAY 는 `enterPlay`가 바로 판 장면으로 | 천천히 썰기 음식 재질은 투과 세기를 0으로 내리지 않는다(0을 지나면 `USE_TRANSMISSION` 프로그램이 바뀌어 재컴파일). 그래서 장면 섞임 0(메인 화면)에서도 투과 세기 1e-3으로 남고, three가 매 프레임 투과용 화면을 한 번 더 그린다. three 투과 패스는 불투명 물체를 톤 매핑 없이 렌더 타깃에 다시 그리므로 불투명 재질마다 렌더 타깃용 프로그램 변형이 따로 필요하다(`SlowStage.precompile`이 바닥의 그 변형을 따로 데우는 이유). 지금은 메인 화면이 이 패스를 계속 그려 그 변형들이 앱 시작부터 컴파일돼 있다 | | 등급 하향 재컴파일 | `src/core/slow/SlowKnifeRig.ts`(유령 칼날 `knifeGhost.visible = !slowTransmission(tier)`), `SlowScene.setTier` → `SlowStage.setTransmission`(바닥 `visible`만 바꿈) | 투과 없는 low 음식 재질은 A의 `requestTier`가 이미 `keepJelly`로 쥔다. 남은 유력 원인은 low 에서만 보이는 유령 칼날(투명 `MeshStandardMaterial`)이다: medium 에서는 한 번도 그려지지 않고 숨긴 물체라 `compileAsync`에서도 빠진다. 천천히 썰기 판 하향은 실기기에서 잰 적이 없다 | | 칼자국 재생성 | `src/cutting/ScalpelGroove.ts` `setPath`, `src/core/knife/ScalpelCutFx.ts` `follow`·`updateStroke` | 긋는 동안 점 수가 바뀔 때마다(회전판은 각도가 바뀔 때마다 매 프레임) 기존 칼자국 메시·재질을 모두 해제하고 경로 배열·지오메트리·`ShaderMaterial`을 새로 만든다. A가 셰이더는 보관해 재컴파일은 없앴지만 할당은 남았다 | ## 3. 접근 방식 - **접근 1(채택): 항목별 맞춤 수정.** 0단계 측정으로 순서와 강도를 정하고, 기존 구조 안에서 각 항목을 고친다. 항목마다 단위 테스트와 측정으로 효과를 따로 확인한다. 투과·등급 하향은 A의 셰이더 보관소(`ShaderKeeper`)를 재사용해 외관 변화·재컴파일 없이 해결한다. - 접근 2(기각): 공용 기반(레이아웃 읽기 일정 관리자, 객체 풀, 렌더 패스 제어)을 먼저 만들고 옮긴다. 이번 다섯 항목에 비해 범위와 위험이 크다. - 접근 3(기각): 눈에 띄는 항목(투과·칼자국)만 고친다. "개선 여지가 있으면 포함"과 맞지 않는다. ## 4. 항목별 설계 ### 4.1 DOM 레이아웃 읽기 - 목표: 평상시 프레임(입력·오버레이 이동·튜토리얼 포함)에 **JS가 일으키는 강제 동기 레이아웃이 없도록** 한다. 카메라 맞춤·화면 좌표 결과는 지금과 같아야 한다. - 캔버스 위치: 캔버스는 화면에 고정이므로 크기 변경(`onResize`, `visualViewport` resize)·방향 전환 때만 다시 읽어 기억하고, `jellyToScreen`·`plateToScreen`·`render.canvasRect()`·입력 변환은 기억한 값을 쓴다. - HUD·메뉴 요소: 같은 프레임 안에서는 한 번만 읽는다(프레임 단위 기억). 가능하면 그 프레임의 DOM 쓰기(HUD 문구·클래스 변경) 앞에서 읽도록 순서를 정리한다. 요소 크기가 실제로 바뀌는 사건(메뉴·결과 카드·패널 열고 닫기, 크기 변경)에는 기억을 비운다. 구체 방식(프레임 기억, `ResizeObserver` 등)은 0단계 측정을 보고 빌더가 정하되, 기억이 낡아 카메라 맞춤이 어긋나는 일이 없어야 한다. - 디버그·E2E 좌표 API(`GameDebug`)는 지금처럼 매번 읽어도 된다(측정 경로 밖). ### 4.2 천천히 썰기 할당 - 0단계에서 천천히 썰기 중 매 프레임 할당을 찾는다(데스크톱 Chrome 할당 기록 또는 힙 샘플링, 실기기 GC 빈도). 매 프레임 경로의 임시 객체·배열·펼침을 재사용 객체로 바꾼다. - 바꾼 함수는 결과값이 지금과 같아야 한다(단위 테스트로 비교). 넘겨받는 쪽이 재사용 객체를 저장하지 않는지 확인한다. ### 4.3 메인 화면 투과 패스 - 투과 세기가 하한(`SLOW_TRANS_MIN`) 근처라 보이지 않는 구간에서는 **투과 없는 셰이더 변형**(`transmission = 0`)으로 바꿔 three의 투과용 추가 그리기를 없앤다. 세기가 올라가면 투과 변형으로 돌아간다. - 두 변형(투과 켬·끔)은 천천히 썰기 음식 재질이 있는 판 동안 셰이더 보관소에 미리 쥐어, 바꿀 때 재컴파일이 없게 한다(A의 `keepJelly` 규칙, 그리기 안 막힘 0). - 전환 경계(어느 세기에서 끄고 켤지, 깜박임 방지 여유)는 빌더가 정한다. 두 변형의 화면 결과가 경계에서 같아야 한다: 하한 세기의 투과 변형과 투과 없는 변형을 같은 장면에서 그려 화소 비교한다(5절). - 메인 화면 외 장면(천천히 썰기 판 진입·이탈의 장면 섞임 0↔1 전환)에서도 같은 규칙이다. - **PLAY 재컴파일 방지(사용자 결정):** 투과 패스를 끄면 불투명 재질의 렌더 타깃용 변형이 PLAY 첫 프레임에 컴파일될 수 있다. 메인 화면(또는 투과 판이 화면에 들어오는 장면) 진입 뒤 투과 패스를 한 번 그려 이 변형들을 데운 뒤 끈다. 데운 변형은 재질이 살아 있는 동안 three 가 재질별로 유지한다. 확인: 천천히 썰기 홈 판에서 PLAY 순간의 그리기 안 막힘 0. - 구현 메모(설계자 판단): `setSlowTransmission`은 지금 `mat.transmission <= 0`으로 투과 재질을 가리므로 투과를 0으로 내리면 판별이 깨진다. 투과 재질 여부는 `uSlowTransK` 유니폼 등 다른 표식으로 가린다. `jellyKeepKey`에는 투과 사용 여부를 넣어 두 변형을 따로 쥔다. ### 4.4 천천히 썰기 등급 하향 재컴파일 - 범위(사용자 결정): 천천히 썰기 판에서 FPS 하향(medium→low 등) 순간의 **모든 그리기 안 컴파일**. 유령 칼날처럼 low 에서만 보이는 무대 자원도 포함한다. 하향 대상 등급에서 새로 보이는 물체의 셰이더를 등급 요청의 보관 단계(A의 `requestTier` → 보관 뒤 확정)에 포함하거나 그 판 동안 미리 쥔다. 방식은 빌더가 정한다. - 0단계에서 천천히 썰기 판 하향의 그리기 안 막힘을 먼저 잰다. **이미 0이면 "해당 없음"으로 기록하고 이 항목을 뺀다.** - 확인: A의 셰이더 막힘 계측으로 천천히 썰기 판 하향 순간의 그리기 안 막힘이 0이어야 한다. ### 4.5 메스 칼자국 재생성 - 긋는 동안 칼자국 메시·재질을 새로 만들지 않는다. 조각별 칼자국 재질은 재사용하고, 지오메트리 버퍼는 용량을 늘려 가며 제자리에서 갱신한다(`setDrawRange` 등). 해제는 칼자국을 지우거나 조각이 바뀔 때만 한다. - 회전판에서 각도만 바뀌는 매 프레임 갱신도 같은 경로를 쓴다. 경로 배열 할당(`path.map`)도 재사용 배열로 줄인다. - 결과 정점 데이터는 지금 `grooveGeometry`가 만드는 것과 같아야 한다(단위 테스트 비교). - 구현 메모(설계자 판단): 칼자국 재질은 조각 재질의 유니폼 객체(`uSquash`·`uShear`·`uCenter`·`uThickness`)를 참조한다. 재질을 재사용하면 등급 교체로 조각 재질이 바뀔 때(`replaceMaterial`) 유니폼을 다시 연결한다. ## 5. 동치·정확성 기준 - 4.1: 카메라 맞춤 결과(카메라 위치·투영)와 화면 좌표 변환이 기억 전·후 같다. 크기 변경·메뉴·결과 카드·패널 열고 닫기 뒤에도 같다(E2E에서 기존 레이아웃 테스트 통과). - 4.2·4.5: 바꾼 함수의 출력이 기존과 같다(단위 테스트). - 4.3: 하한 세기 투과 변형과 투과 없는 변형을 같은 메인 화면에서 그린 화소 비교가 판정선 안이다(판정선은 빌더가 기존 시각 E2E 관례에서 정하고 계획에 적는다). 넘으면 멈추고 사용자에게 시각 검수 방식을 묻는다. - 4.3·4.4: 그리기 안 막힘 0(A 계측). ## 6. 측정 설계 ### 6.1 0단계(기준선) - B와 같은 방식으로 실기기 장면을 `e2e/helpers/`·`e2e/device/`에 추가하고 `scripts/device-ab.sh`(네 번째 인자로 묶음 선택)로 돌린다. 장면마다 정해진 시간 창의 프레임 간격을 페이지 안 `requestAnimationFrame` 기록으로 모아 **평균·p95**를 남긴다. 보조 지표로 **게임 루프 메인 스레드 작업 시간**(`Game.frame` 시작~렌더 제출 끝, 디버그 계측으로 노출)의 평균·p95도 남긴다(요약 표에 열 추가). - 장면(대표 판·창 길이는 빌더가 정하고 계획에 적는다): 1. 메인 화면 대기: 홈 판이 천천히 썰기 판(투과 재질)일 때와 일반 판일 때(대조군) 2. 천천히 썰기 중: 썰기 끌기를 일정 시간 이어 가는 구간 3. 메스 긋는 중: 일반 판 메스 끌기, 회전판 메스 누른 채 멈춤 4. 결과 카드 등장: 결과 카드가 올라오는 구간(오버레이 이동) 5. 천천히 썰기 등급 하향: 판 안에서 `requestTier('low')` 순간의 최대 간격·그리기 안 막힘 6. 천천히 썰기 홈 판에서 PLAY: PLAY 직후 구간의 최대 간격·그리기 안 막힘(4.3 회귀 감시) - 에뮬레이션: 데스크톱·모바일 성능 E2E에 1~4의 평균·p95를 **기록만** 하는 측정을 추가한다. 새 단언 예산은 두지 않는다. - 0단계에서 원인 비중도 본다: 강제 동기 레이아웃(기기 Chrome 트레이스의 `Layout` 이벤트와 JS 호출 위치), 천천히 썰기 GC(트레이스의 GC 이벤트), 투과 패스(트레이스 GPU 시간 또는 투과 패스를 끈 비교). 결과를 계획 문서에 적고 순서·강도를 정한다. ### 6.2 완료 기준 - 실기기: `scripts/device-ab.sh <기준> <변경> 3 "<묶음>"`으로 번갈아 3회. 장면별 평균·p95 중앙값의 전·후 값과 개선 폭을 계획 문서와 `PROGRESS.md`에 기록한다. - 장면 1~4: 프레임 간격의 평균·p95가 기준보다 나빠지지 않고, **프레임 간격 또는 작업 시간**의 평균·p95 중 하나가 개선된다(흔들림 판정은 `refactor-baseline.md` 6절 max(1ms, 20%)). 둘 다 변화가 없으면(화면 주사율에 묶임 등) 원인과 함께 기록하고 머지 전에 알린다. - 장면 5: 그리기 안 막힘 0, 최대 간격이 개선된다(기준선 막힘 0이면 해당 없음). - 장면 6: 그리기 안 막힘 0, 최대 간격이 기준보다 나빠지지 않는다. - 조건을 못 지킨 장면은 원인과 함께 기록하고 머지 전에 사용자에게 알린다. - 에뮬레이션: 기존 `perf.spec` 지표 회귀 없음(같은 규칙), 예산 완화 없음. 단위 전체·빌드·`npm run e2e:mobile` 통과. - 시각: 4.3의 자동 화소 비교 외에 외관 판단은 없다. 외관을 바꾸는 수정이 필요해지면 멈추고 시각 검수 방식을 사용자에게 묻는다. - Native/SDD 코드 리뷰와 최종 독립 리뷰(Claude Code는 Opus / medium)를 유지한다. - 마무리: `PROGRESS.md` 갱신 후 로컬 main 머지. 푸시하지 않는다. ## 7. 비목표와 피해야 할 결과 비목표: - B의 남은 것(고양이 발 모델 생성 결과 카드 끊김, 찾기 진입·찍기의 그리기 끊김) - 렌더링 품질 등급·DPR·그림자 설정 변경, `FpsGovernor` 정책 변경 - 투과 패스 해상도·빈도 낮추기(외관 변화) - iPhone 측정 - main 푸시·운영 배포 피해야 할 결과: - 외관·판정 변화, 투과 변형 전환 때 깜박임 - 낡은 레이아웃 기억으로 카메라 맞춤·입력 좌표가 어긋나는 것 - 재사용 객체를 받는 쪽이 저장해 값이 덮이는 것 - 셰이더 보관소가 판이 끝나도 투과 변형을 계속 쥐는 것(판 범위로 해제) - 성능 예산·품질 임계값 완화 - 기기 측정이 기본 E2E·CI에 섞이는 것 ## 8. 빌더에게 맡기는 것 - 0단계 장면의 대표 판·창 길이, 원인 비중 측정 방법 - 4.1 기억 방식과 무효화 시점 - 4.2 바꿀 할당 목록(0단계 근거) - 4.3 전환 경계와 깜박임 방지, 화소 비교 판정선 - 4.4 하향 변형을 쥐는 방식 - 4.5 버퍼 용량 증가 방식 - 작업 순서(0단계 측정 근거)