Skip to content

멀티 렌더러 지원 트래킹 이슈 #536

@seo-rii

Description

@seo-rii

제안 내용

멀티 렌더러를 안정적으로 지원할 수 있도록 렌더링 구조를 단계적으로 분리합니다.

기존에는 각 renderer가 PageRenderTree를 직접 해석하는 흐름에 가까웠습니다. 앞으로는 renderer backend가 공통으로 replay할 수 있는 PageLayerTree를 중간 IR로 두고, SVG / Canvas2D / native Skia / CanvasKit / PDF export backend가 같은 layer contract를 기준으로 동작하게 합니다.

대략적인 흐름은 아래와 같습니다.

Document / Layout
-> PageRenderTree
-> PageLayerTree
-> SVG / Canvas2D / CanvasKit / native Skia / PDF export backend

이 이슈는 멀티 렌더러 작업을 리뷰 가능한 PR 단위로 나누어 추적하기 위한 tracking issue입니다. 기준은 단순히 변경 파일 수를 줄이는 것이 아니라, 각 PR이 독립적으로 설명 가능한 contract를 갖도록 만드는 것입니다.

분할 기준

  • 기본 동작을 바로 바꾸지 않는 구조 변경은 먼저 넣습니다.
  • public rendering path가 바뀌는 PR은 별도로 둡니다.
  • path 전환 뒤에는 visual/pixel diff 같은 검증 pipeline을 따로 붙입니다.
  • native Skia backend는 Canvas 전환과 섞지 않고 별도 backend PR로 둡니다.
  • native Skia 안에서도 equation, raw SVG, form control, text, image effect처럼 실패 양상이 다른 leaf replay는 독립적으로 검토합니다.
  • foundation만 추가하고 consumer가 없는 phase는 가능하면 다음 consumer 작업과 합칩니다.
  • Text IR v2는 source table, placement/cluster metadata, special visual externalization을 각각 쪼개지 않고 TextRun compatibility contract 단위로 묶습니다.
  • GlyphRun은 당분간 canonical path가 아니라 optional variant로만 둡니다. TextRun fallback이 항상 남아 있어야 합니다.
  • GlyphRun foundation, font resource, native Skia guard는 같은 variant contract로 보고 한 묶음으로 검토합니다. CanvasKit guard와 GlyphOutline은 둘 다 text variant adoption 문제라 같은 후속 PR로 묶습니다.
  • GlyphOutlineGlyphRun의 단순 확장이 아니라 strict-visual sidecar contract로 봅니다. 다만 reviewer 입장에서는 CanvasKit GlyphRun adoption과 같은 text variant backend phase에서 같이 보는 편이 낫습니다.
  • variantOps와 richer outline stroke payload는 schema migration 문제입니다. 별도 소형 PR로 빼기보다 Text IR v2 closure phase 안에서 dual-reader/single-writer 정책까지 같이 정리합니다.
  • render: add native Skia PNG raster backend #599 이후 들어온 export-png / VLM 사용 흐름은 존중하되, VLM preset 확장이나 PNG metadata 같은 제품 요구사항은 별도 PR로 둡니다.
  • cache/resource/text/image-effect 작업은 wholesale merge하지 않고 correctness -> IR contract -> guarded backend adoption -> diagnostics/performance 순서로 다시 자릅니다.
  • Studio/CanvasKit 쪽 helper나 E2E 변화는 backend adoption 또는 renderer sweep에 필요한 조각만 가져옵니다. editor UI/compare/history 정리는 이 시리즈에 섞지 않습니다.
  • PDF export는 PNG raster export와 다른 출력 계약으로 보고 별도 PR로 둡니다. 다만 SVG->PDF MVP와 direct/vector hardening은 같은 PDF backend contract 안에서 같이 봅니다.
  • native binding/Swift package는 별도 사용자 표면입니다. native render/export API가 추가될 때마다 Swift/XCFramework/FFI 문서 영향 여부를 같이 확인합니다.

최신 확인 결과 (2026-05-14)

Text IR v2 설계 상태

P11은 이 시리즈에서 Text IR v2 개념이 처음 들어오는 지점입니다. 다만 이 단계에서 Text IR v2를 완성하거나 기존 text replay contract를 대체하지는 않습니다. P11은 기존 TextRun fallback을 유지한 채, 후속 GlyphRun/font resource/native glyph replay가 붙을 수 있는 compatibility contract를 먼저 여는 단계입니다.

Text IR v2의 핵심 목표는 아래와 같습니다.

  • 원문 source 추적성을 보존합니다. PageLayerTree.text_sourcesTextRun.source span을 통해 backend/debug consumer가 text op가 어떤 원문 범위에서 왔는지 알 수 있어야 합니다.
  • 기존 TextRunNode payload에 섞여 있던 layout/replay 의미를 더 명확히 나눕니다. paintStyle, projectionKind, orientation, placement, clusterBasis, clusters, legacyVisuals 같은 metadata를 통해 backend가 text placement와 compatibility fallback을 구분할 수 있게 합니다.
  • schema negotiation을 명시합니다. schemaMinorVersion, resourceTableMinorVersion, usedFeatures, requiredFeatures, optionalFeatures, knownFeatures로 consumer가 자신이 처리 가능한 feature와 fallback 필요 여부를 판단할 수 있어야 합니다.
  • special text visuals를 외부 paint op로 분리합니다. char overlap, visible control mark, tab leader, underline/strike/emphasis 같은 visual은 TextRun 내부 payload에만 묻히지 않고 PaintOp::CharOverlap, TextControlMark, TabLeader, TextDecoration으로 별도 표현합니다.
  • 향후 exact glyph replay를 위한 자리를 만듭니다. GlyphRun은 아직 기본 경로가 아니며, font identity와 cluster mapping이 검증된 경우에만 선택 가능한 optional variant로 계획합니다.

현재 진행 상태는 아래처럼 봅니다.

  • P11: Text IR v2 compatibility contract 초안입니다. source table, source span, placement/cluster metadata, special visual op externalization, feature negotiation metadata를 추가합니다.
  • P11: 기존 SVG / Canvas / native Skia renderer는 새 special visual op를 skip합니다. 이는 새 op가 기존 TextRun fallback과 중복으로 그려지는 것을 막기 위한 guard입니다.
  • P11: JSON/schema field 이름과 feature taxonomy는 앞으로 consumer 구현을 붙이면서 조정될 수 있습니다. 그래서 major schema를 고정하는 완성 단계가 아니라 minor/additive contract를 여는 단계로 둡니다.
  • P12: Guarded GlyphRun variant contract가 PR render: add guarded GlyphRun layer variant contract #840 기준으로 devel에 반영됐습니다. TextRun fallback을 유지한 채 GlyphRun을 optional sidecar variant로 추가했고, native Skia는 exact blob-backed typeface replay가 들어오기 전까지 GlyphRun을 선택하지 않습니다.
  • P13: Text IR v2 closure입니다. fallback reason, validation issue, line-break risk telemetry, fallback-free profile gate, JS/JSON compatibility export, v1/v2 downgrade policy, variantOps dual-reader/single-writer migration을 같은 안정화 묶음으로 봅니다.
  • P14: text variant backend adoption입니다. CanvasKit GlyphRun guarded replay와 GlyphOutline/outline stroke strict sidecar를 같이 보되, TextRun fallback과 deterministic fallback reason을 유지합니다.
  • P15: image/object replay hardening입니다. image effect/cache/resource diagnostics와 HWP3 picture/WMF/EMF/OLE object replay를 같은 native leaf replay 축으로 묶습니다.
  • P16: PDF export와 native render/export API packaging입니다. PDF backend가 Swift/native binding 영향 없이 Rust-only로 끝나지 않게 같이 봅니다.
  • P17: full renderer sweep/resource transport/performance report입니다. 여러 backend가 모인 뒤 같은 fixture/corpus 기준으로 비교합니다.

P11의 비목표도 명확히 둡니다.

  • GlyphRun을 canonical text path로 전환하지 않습니다.
  • font blob / font face resource table을 완성하지 않습니다.
  • native Skia glyph id replay나 CanvasKit glyph replay를 기본 경로로 넣지 않습니다.
  • public render path를 바꾸지 않습니다.
  • Text IR v2 schema를 최종 안정화했다고 선언하지 않습니다.

현재 진행상황

  • P1: 렌더러 프론트엔드/백엔드 1차 분리

  • P2: Canvas 렌더링을 PageLayerTree 기반으로 전환

  • P3: Canvas visual diff 테스트 추가

  • P4: native Skia PNG raster backend 추가

    • native-skia feature 추가
    • PageLayerTree 기반 Skia replay 구현
    • native-only PNG export API 추가
    • raster guard / image replay / fallback replay / native Skia smoke test 추가
    • 관련 PR: render: add native Skia PNG raster backend #599
  • P5: native Skia equation replay 추가

  • P6: native Skia raw SVG fragment replay

  • P7: native Skia form control static replay

    • push button / checkbox / radio button / combo box / edit field 정적 drawing
    • PNG/export 경로에서 form object가 placeholder로만 보이지 않도록 보강
    • 관련 PR: Skia form control static replay (P7) #740
  • P8: Layer IR schema/resource key hardening

    • paint/schema.rs 추가
    • layer schema metadata와 resource key versioning 추가
    • wasm export contract test 추가
    • layer output option consumption을 renderer 경로에서 확인
    • 이후 IR v2 / text / glyph / resource 작업이 같은 metadata와 key contract에 기대게 됩니다.
    • 관련 PR: render: harden PageLayerTree schema and resource keys #761
  • P9: native Skia text replay parity + module split

    • native Skia text replay가 기존 TextRunNode의 char overlap, tab leader, control mark, decoration, vertical rotation 정보를 소비하도록 보강
    • 커진 text replay 구현을 renderer/skia/text_replay.rs로 분리
    • IR v2 전에 v1 compatibility replay 품질을 올리는 단계입니다.
    • 관련 PR: render: improve Skia text replay parity #769
  • P10: P9에 fold

    • 별도 PR로 열기에는 순수 리팩터링에 가까워서 P9에 포함했습니다.
    • text replay module split은 #769에 들어갔습니다.
  • P11: Text IR v2 compatibility contract

    • 관련 PR: render: add Text IR v2 compatibility contract #797
    • PageLayerTree 최상위에 schema/resource table minor version과 feature negotiation metadata를 추가합니다.
    • usedFeatures, requiredFeatures, optionalFeatures, knownFeatures로 consumer가 지원 범위를 판단할 수 있게 합니다.
    • PageLayerTree.text_sources와 export-local TextSourceTable을 추가합니다.
    • TextRun.source span을 export해서 backend/debug consumer가 원문 source range를 추적할 수 있게 합니다.
    • TextRun.paintStyle, projectionKind, orientation, placement, clusterBasis, clusters, legacyVisuals를 export합니다.
    • PaintOp::CharOverlap, PaintOp::TextControlMark, PaintOp::TabLeader, PaintOp::TextDecoration을 추가합니다.
    • LayerBuilder에서 char overlap / tab leader / decoration / visible control mark를 explicit visual op로 낮춥니다.
    • 기존 SVG / Canvas / native Skia renderer는 새 special visual op를 skip해서 double-painting을 방지합니다.
    • docs/text-ir-v2.md에 Text IR v2 migration contract를 문서화했습니다.
  • P12: Guarded GlyphRun variant contract

    • 관련 PR: render: add guarded GlyphRun layer variant contract #840 (cherry-pick/squash 후 closed)
    • devel merge commit: 249e5653; squashed feature commit: 08af19b6
    • TextRun fallback을 유지한 상태에서 GlyphRun을 optional sidecar variant로 도입했습니다.
    • fontResources, font blob, face identity, replay eligibility 타입을 추가했습니다.
    • TextShapeLowerer를 추가하되, 기본 lowering은 TextRun fallback을 유지합니다.
    • shaping resolver가 export 가능한 glyph data를 줄 때만 GlyphRun sidecar를 붙입니다.
    • native Skia는 contract guard만 유지하고, exact blob-backed typeface replay가 들어오기 전까지 GlyphRun을 선택하지 않습니다.
    • Canvas2D/layered SVG transition path는 GlyphRun sidecar를 건너뛰고 fallback replay를 유지합니다.
    • CanvasKit glyph replay, glyph outline/stroke payload, variantOps 기반 고급 sidecar 선택, 실제 문서 font blob 추출은 후속으로 둡니다.

다음 분할 후보

  • P13: Text IR v2 diagnostics, schema closure, and variantOps

    • P12에서 열린 GlyphRun/fallback 선택 결과를 diagnostics와 schema profile로 안정화합니다.
    • 작게 쪼개져 있던 variantOps/schema v2 compatibility work도 같은 Text IR v2 closure phase로 합칩니다.
    • variant selection report, fallback reason, missing glyph, cluster mismatch, unsupported effect reason을 같은 report shape로 정리합니다.
    • TextShapeLowerer 노출 범위와 shaped measurement profile boundary를 문서화합니다.
    • schema v1/v2 compatibility export, validation issue, slot diagnostics, downgrade path를 정리합니다.
    • fallback-free profile은 strict variant metadata가 있을 때만 통과하도록 guard합니다.
    • line-break context는 known, knownAbsent, unknown을 구분합니다. 필요한 context가 unknown이면 insufficientContext로 fail closed합니다.
    • standalone TextShapeReport는 local/nightly migration telemetry로 보고, layer replay schema의 일부로 보지 않습니다.
    • stable text variant op id, sidecar anchor validation, cross-scope part reject rule을 포함합니다.
    • v2-to-v1 downgrade는 faithful TextRun fallback과 same-scope v1 payload가 있을 때만 허용합니다.
    • fallback-free strict export나 unsupported richer payload는 silently degrading하지 않고 fail closed합니다.
    • writer는 한 export 안에서 root variant ops와 sidecar variantOps 중 하나만 emit하는 single-writer 규칙을 둡니다.
    • Canvas2D/SVG/native Skia가 알 수 없는 optional text variant와 diagnostics를 안전하게 건너뛰는지 확인합니다.
  • P14: Text variant backend adoption

    • CanvasKit GlyphRun guarded replay와 GlyphOutline strict sidecar는 둘 다 text variant를 실제 backend가 어디까지 선택할 수 있는지 정하는 단계입니다.
    • CanvasKit font blob resource transport, producer hash, digest mismatch fallback, face index 제한을 검증합니다.
    • fill-only exact replay를 기본 성공 케이스로 두고, shadow/outline은 기존 TextRun 효과와 parity가 확인된 작은 subset만 gated replay로 엽니다.
    • underline/strike/emphasis, ratio, shade, emboss/engrave, variation face, non-zero face index, glyph id range 초과는 deterministic fallback reason을 남깁니다.
    • GlyphOutline은 generic Path가 아니라 strict-visual text variant로 추가합니다.
    • anchorOpId, 같은 leaf의 TextRun fallback, variant metadata를 필수 조건으로 둡니다.
    • schema v1에서는 monochrome fill-only outline을 기본 replay-eligible로 둡니다.
    • outline stroke subset은 validation, JSON/JS export, SVG/Canvas2D subset replay, native strict path validation, cache key까지 같이 봅니다.
    • strict outline payload는 generic Path와 섞지 않고 text variant diagnostics/fallback reason을 공유합니다.
    • native Skia vs CanvasKit parity sweep은 report-only fuzzy 기준으로 시작하고, public default path 전환은 하지 않습니다.
  • P15: Image, cache, and native object replay hardening

    • image replay correctness, cache/resource diagnostics, HWP3 picture/WMF/EMF/OLE object replay는 모두 native leaf replay의 실패 양상을 좁히는 단계입니다.
    • crop/tile scaling, black-white preprocessing, pattern8x8 dithering, alpha preservation, binary image effect parity를 묶습니다.
    • browser/native image effect preprocessing 기준을 맞추고, pattern reference fixture를 공유합니다.
    • image/cache key가 page scale, source rect, effect, alpha, sampling option, resource digest를 반영하도록 합니다.
    • static picture cache, replay cache, static cache key, resource fingerprint split을 같이 정리합니다.
    • image effect timing/memory/offscreen/backend diagnostics와 reset semantics를 추가합니다.
    • HWP3 picture alignment, WMF/metafile coordinate order, EMF/OLE chart 계열 native replay를 같은 object replay 축으로 봅니다.
    • unsupported object는 기존 fallback을 유지합니다.
    • chart/image/object bbox, transform, clip, fallback placeholder behavior를 테스트합니다.
  • P16: PDF export backend and native API packaging

    • PDF는 PNG raster export와 다른 출력 계약입니다. 다만 PDF export가 native 사용자 표면으로 노출될 가능성이 높으므로 Swift/native binding impact와 API packaging을 같은 phase에서 봅니다.
    • native-only SVG->PDF export를 먼저 추가합니다.
    • 단일/다중 페이지, page range, page size, font fallback, output path handling, error reporting을 테스트합니다.
    • shortcut.hwp PDF 정합성 잔여 결함 4건 (헤더 표 spacing / 좌여백 / 단 구분선 점선 / 우측탭 오버플로) #842/#843에서 정리된 shortcut.hwp PDF 기준을 fixture로 반영합니다.
    • 이어서 PageLayerTree에서 직접 PDF/vector recording으로 가는 경로를 검토합니다.
    • Skia PDF surface와 별도 PDF writer backend 중 어떤 쪽이 renderer contract에 맞는지 비교합니다.
    • print/high-quality profile, vector/raster fallback policy, embedded image/font resource 처리, page box/metadata contract를 정리합니다.
    • PNG/PDF/layer tree export API가 Rust-only로 끝나지 않도록 Swift 문서와 package script를 확인합니다.
    • C ABI export와 native package release workflow는 renderer backend 안정화 뒤에 묶습니다.
  • P17: Full renderer sweep, resource transport, and performance gates

    • SVG / Canvas2D / native Skia / CanvasKit / PDF export를 같은 fixture/corpus 기준으로 비교할 수 있는 manual sweep을 정리합니다.
    • renderer baseline manifest와 capture/report script를 정리합니다.
    • native/browser baseline performance diagnostics를 artifact로 남깁니다.
    • layer resource hash, skipped payload, shared font/image/SVG resource transport, cache invalidation diagnostics를 한 곳에서 봅니다.
    • performance metrics는 default CI gate가 아니라 report artifact로 먼저 둡니다.
    • Studio e2e/lifecycle/perf helper는 renderer sweep에 필요한 부분만 가져오고, editor UI/compare/history 정리는 이 renderer 분할 작업에 섞지 않습니다.

합친 phase

  • P10은 P9에 합쳤습니다. 순수 리팩터링이라 별도 리뷰 단위로 두면 같은 text replay 코드를 두 번 보게 됩니다.
  • 기존 P11/P12/P13은 P11로 합쳤습니다. source table, placement/cluster metadata, special visuals externalization은 모두 TextRun v2 compatibility contract를 완성하는 한 묶음입니다.
  • 기존 P14/P15/P16은 P12로 합쳤습니다. font resource table만 따로 넣으면 consumer가 없는 foundation PR이 되고, native Skia guard는 같은 GlyphRun variant contract입니다.
  • P13은 diagnostics/shape profile/schema closeout에 variantOps/schema v2 compatibility까지 합칩니다. doc-only, test-only, fallback reason-only, reader-only PR로 쪼개지 않습니다.
  • 기존 P14/P15는 P14로 합칩니다. CanvasKit GlyphRun guarded replay와 GlyphOutline strict sidecar는 둘 다 text variant backend adoption 단계입니다.
  • 기존 P17/P18은 P15로 합칩니다. image effect/cache/resource diagnostics와 HWP3 picture/WMF/EMF/OLE object replay는 native leaf replay 안정성이라는 같은 reviewer context를 공유합니다.
  • 과거 PDF 관련 P19/P21은 P16으로 합칩니다. PDF export backend와 native render/export API packaging은 사용자 표면이 이어져 있으므로 같이 검토합니다.
  • 기존 P20은 P17로 당깁니다. full renderer sweep/performance report는 여러 backend가 모인 뒤의 검증 phase라 별도 유지합니다.

별도 트래킹

완료 기준

  • public Canvas 렌더링이 PageLayerTree 기반으로 안정적으로 동작함
  • Canvas legacy vs layer Canvas visual diff pipeline이 CI에서 동작함
  • native Skia backend가 feature-gated 상태로 추가되고 기본 PNG 렌더링 테스트를 통과함
  • native Skia PNG 경로에서 equation replay가 동작함
  • native Skia PNG 경로에서 raw SVG fragment replay가 동작함
  • native Skia PNG 경로에서 form control static replay가 동작함
  • PageLayerTree JSON / JS export가 backend replay에 필요한 주요 정보를 안정적으로 보존함
  • Text IR v2 compatibility contract가 source table, placement/cluster metadata, special visual ops를 함께 보존함
  • GlyphRun foundation이 들어가되 TextRun fallback이 유지되고 native Skia는 exact blob-backed typeface replay 전까지 fallback을 유지함
  • Text IR v2 closure가 fallback reason, feature negotiation, shaped profile boundary, line-break context completeness, v1/v2 downgrade fail-closed policy, variantOps dual-reader/single-writer migration을 한 schema contract로 설명함
  • CanvasKit GlyphRun gate와 GlyphOutline strict sidecar가 TextRun fallback, deterministic fallback reason, anchor/fallback/monochrome fill-only 조건을 공유하고 generic Path로 섞이지 않음
  • Skia image effect/cache/resource diagnostics와 HWP3 picture / WMF / EMF / OLE native object replay가 fallback을 보존하면서 동작함
  • PDF export backend가 단일/다중 페이지, page range, page metadata, vector/raster fallback policy를 안정적으로 처리하고 native binding packaging 기준에서도 설명 가능함
  • SVG / Canvas2D / native Skia / CanvasKit / PDF export backend를 같은 IR 기준으로 비교할 수 있음
  • 주요 fixture에 대한 visual regression 테스트와 performance report가 CI 또는 artifact workflow에서 동작함

참고

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions