render invalidation과 dirty flag
작은 editor는 매 pointer move마다 전체 scene을 다시 그려도 됩니다. 하지만 문서가 커지면 어떤 변경이 어떤 renderer 작업을 다시 만들어야 하는지 구분해야 합니다.
dirty flag를 책임별로 나눈다
하나의 dirty = true로 시작해도 되지만, 실제 렌더러에서는 bounds, draw list, GPU buffer, texture cache가 서로 다른 비용을 가집니다.
const Dirty = {
Render: 1 << 0,
Bounds: 1 << 1,
DrawList: 1 << 2,
GpuBuffer: 1 << 3,
Texture: 1 << 4
};
function markNodeDirty(state, nodeId, flags) {
const previous = state.dirtyByNode.get(nodeId) ?? 0;
state.dirtyByNode.set(nodeId, previous | flags);
state.needsFrame = true;
}
position 변경은 bounds와 GPU buffer를 더럽히지만, text 내용 변경은 texture도 더럽힐 수 있습니다.
function updateNodeTransform(state, nodeId, matrix) {
updateNode(state.document, nodeId, { localMatrix: matrix });
markNodeDirty(state, nodeId, Dirty.Bounds | Dirty.DrawList | Dirty.GpuBuffer | Dirty.Render);
}
function updateTextContent(state, nodeId, text) {
updateNode(state.document, nodeId, { text });
markNodeDirty(state, nodeId, Dirty.Bounds | Dirty.Texture | Dirty.GpuBuffer | Dirty.Render);
}
render request와 rebuild를 분리한다
상태가 바뀔 때마다 즉시 렌더링하지 않습니다. 변경은 dirty로 표시하고, 다음 animation frame에서 필요한 작업만 처리합니다.
function requestRender(state) {
if (state.frameRequested) return;
state.frameRequested = true;
requestAnimationFrame(() => {
state.frameRequested = false;
rebuildDirtyResources(state);
renderer.render(createSnapshot(state));
clearDirtyFlags(state);
});
}
이 구조를 두면 pointer move가 한 프레임에 여러 번 들어와도 renderer 작업은 한 번으로 합쳐집니다.
오늘의 핵심
dirty flag는 성능 최적화만이 아닙니다. editor state 변경을 bounds cache, draw list, GPU buffer, texture upload 같은 renderer 작업으로 번역하는 계약입니다.