You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Vulkan Buffer 메모리 전략 (eBufferType / eBufferTransfer / eDataAccess ↔ Vulkan Flags)
작성자: Winteradio 날짜: 2026-08-09 주제:VulkanSystem::InitializeBuffer — eBufferType/eBufferTransfer/eDataAccess를 Vulkan VkBufferUsageFlags/VkMemoryPropertyFlags로 매핑하는 설계 상태: 🟡 설계 확정, 구현 진행 중
배경
VulkanSystem::InitializeBuffer()는 세 개의 독립적인 축을 조합해서 최종 Vulkan 값을 만듦.
eBufferType — 이 버퍼가 어느 GPU 파이프라인 단계에 바인딩되는가 → VkBufferUsageFlags의 역할 비트
eBufferTransfer — 이 버퍼가 copy 명령의 원본/목적지로 쓰일 수 있는가 → VkBufferUsageFlags의 Transfer 비트
eDataAccess + eBufferTransfer — 이 버퍼가 물리적으로 어디에 있어야 하는가 → VkMemoryPropertyFlags
eBufferType — 역할 비트
어느 GPU 파이프라인 단계(고정 기능 유닛 또는 셰이더 바인딩)가 이 버퍼를 읽는지(또는 storage의 경우 읽고 쓰는지)를 나타냄.
vkCmdCopyBuffer 같은 copy 명령은 vertex-fetch나 셰이더 코어와는 별개의 전송(DMA) 엔진이 처리함.
그래서 eBufferType의 역할 비트가 있다고 해서 자동으로 copy 명령에 참여할 수 있는 건 아님 —
draw call에서 vertex로 읽히는 것과 copy 명령에서 원본/목적지로 쓰이는 건 서로 무관한 별개의 용도라
각각 명시적으로 비트를 켜야 함.
eBufferTransfer
VkBufferUsageFlags
의미
eSource
TRANSFER_SRC_BIT
이 버퍼가 copy 명령의 srcBuffer가 될 수 있음 — 전송 엔진이 여기서 읽어감
eDestination
TRANSFER_DST_BIT
이 버퍼가 copy 명령의 dstBuffer가 될 수 있음 — 전송 엔진이 여기로 써줌
비트플래그라 eSource | eDestination 동시 설정도 가능함(스크래치/중간 버퍼 등).
eDataAccess::eStaging는 "GPU 바인딩 역할이 없는 순수 전송 버퍼"를 뜻함(주석 참고: GPU Binding Datas는 eStatic/eDynamic/eStream, GPU Transfer Datas는 eStaging). 이 값과 eBufferTransfer의 방향을
조합하면 업로드용인지 리드백용인지가 구분됨 — 별도로 eReadBack 값을 추가하지 않아도 됨.
eDataAccess
eBufferTransfer
VkMemoryPropertyFlags
설명
eStatic
(무관)
DEVICE_LOCAL
GPU 전용, staging으로 최초 1회만 채움
eDynamic
(무관)
DEVICE_LOCAL|HOST_VISIBLE|HOST_COHERENT
매핑 공유 우선 시도
eStream
(무관)
DEVICE_LOCAL|HOST_VISIBLE|HOST_COHERENT
매핑 공유 우선 시도(큰 경우 재-staging 전략은 InitializeBuffer가 별도 처리)
eStaging
eSource
HOST_VISIBLE|HOST_COHERENT
업로드 버퍼 — CPU가 쓰고 GPU가 복사해감
eStaging
eDestination
HOST_VISIBLE|HOST_CACHED
리드백 버퍼 — GPU가 복사로 써주고 CPU가 읽음
eStatic/eDynamic/eStream은 항상 GPU에 바인딩되는 "진짜" 리소스라 eBufferTransfer 방향과 무관하게
값이 고정됨. eStaging일 때만 방향을 봐서 COHERENT(업로드)냐 CACHED(리드백)냐를 가름.
eDynamic/eStream의 "매핑 공유 우선 시도"는 DEVICE_LOCAL|HOST_VISIBLE|HOST_COHERENT 조합이 용량
제한으로 실패할 수 있다는 뜻 — 그때는 HOST_VISIBLE|HOST_COHERENT만으로 재시도해야 함. 이 폴백은 GetBufferProperty 하나의 반환값으로 표현할 수 없는 실제 할당 시도 로직이라 InitializeBuffer가 m_memory->Allocate() 결과를 보고 처리함.
왜 생성 시점에 별도의 매핑 의도 플래그가 없는가
Unreal Engine의 EBufferUsageFlags도 BUF_Static/BUF_Dynamic/BUF_Volatile을 생성 시점에 한 번 정하고,
그 값 자체가 곧 매핑 가능 여부를 결정함 — 별도로 "나중에 매핑할 거야?"를 또 묻지 않음.
같은 이유로 eDataAccess::eDynamic/eStream이라는 값 자체가 이미 "CPU가 이후에 이 버퍼를 건드릴 것"이라는
의도를 담고 있어서, 생성 시점에 별도의 매핑 의도 신호가 필요 없음. CPU가 생성 후 전혀 안 건드릴 거면
그건 애초에 eStatic이었어야 함. (참고로 eMapAccess는 RHIBufferUpdateDesc에만 있고, 이건 매 Lock/Map
호출마다 정확히 뭘 할 건지를 나타내는 별개의 용도 — GL의 glMapBufferRange access 파라미터와 같은 자리.)
VulkanDevice가 GRAPHICS|COMPUTE|TRANSFER를 만족하는 큐 패밀리 하나만 쓰므로, 여러 큐 패밀리가 나눠 쓰는 상황 자체가 없음
queueFamilyIndexCount
0
EXCLUSIVE일 때 무시됨
pQueueFamilyIndices
nullptr
EXCLUSIVE일 때 무시됨
메모리 속성 유형 — 하드웨어 데이터 전송 경로별 분류
eStatic/eDynamic/eStream/eStaging에 적용되는 다섯 가지 전송 경로.
유형
플래그
데이터 전송 경로
GPU 전용 메모리
DEVICE_LOCAL만
GPU 코어 ↔ VRAM 직접 접근. CPU는 접근 불가, staging 버퍼를 거쳐 복사로만 채움. 가장 빠름
CPU→GPU 논캐시 전송
HOST_VISIBLE | HOST_COHERENT
CPU가 캐시를 거치지 않고(Write-Combining) System RAM에 직접 씀. flush 불필요. GPU는 매 접근마다 PCIe를 거쳐 읽음
GPU→CPU 캐시 리드백
HOST_VISIBLE | HOST_CACHED
GPU가 쓴 데이터를 CPU가 자신의 캐시를 거쳐 반복해서 빠르게 읽음. 읽기 전 invalidate 필요
GPU-CPU 매핑 공유(용량 제한)
DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT
VRAM 중 CPU가 직접 매핑 가능한 좁은 영역(ReBAR 등). GPU는 VRAM 속도, CPU도 직접 접근. 용량이 작아서 못 들어가면 위의 "CPU→GPU 논캐시 전송"으로 폴백
통합 메모리 공유
HOST_VISIBLE | HOST_COHERENT | HOST_CACHED
CPU/GPU가 물리적으로 같은 RAM과 캐시 일관성 하드웨어를 공유하는 구조(통합 메모리 아키텍처) 전용. 본 설계 대상 아님
핵심 규칙 두 가지
1. HOST_COHERENT는 HOST_VISIBLE 없이 단독으로 쓰지 않음.
COHERENT는 CPU가 매핑해서 접근할 때의 캐시 동작을 설명하는 플래그임.
매핑 자체가 안 되는 메모리엔 적용 대상이 없음.
2. DEVICE_LOCAL과 HOST_VISIBLE은 공존 가능하지만 용량이 제한적임(GPU-CPU 매핑 공유).
그래서 "작으면 CPU 쪽, 크면 GPU 쪽"이 아니라 반대에 가까움.
작은 버퍼가 오히려 이 좁은 풀에 들어가서 GPU 속도와 CPU 접근을 동시에 얻고,
큰 버퍼는 이 풀에 못 들어가서 순수 HOST_VISIBLE로 떨어짐.
구현상으로는 "매핑 공유를 먼저 시도하고, 실패하면 논캐시 전송으로 폴백"하는 2단계 로직임.
Write-Combining(WC) — 왜 CPU 쓰기 전용 데이터는 캐시가 없는 편이 더 빠른가
한 번 쓰고 CPU가 다시 읽지 않는 데이터(vertex/uniform buffer 업로드)라면,
일반 캐시를 거치는 경로가 오히려 더 비쌈.
[캐시 있음] Read-For-Ownership(RFO) -> 쓰기 -> 나중에 write-back
[캐시 없음, WC] 쓰기(작은 버퍼에 모음) -> 한 번에 flush
RFO가 낭비인 이유
일반 write-back 캐시는 특정 캐시라인에 쓰기를 하려면,
그 라인을 캐시에 온전히 올려야 dirty 표시를 할 수 있음.
아직 캐시에 없는 라인이면 CPU는 먼저 그 라인을 메모리에서 읽어와서(RFO) 캐시에 올린 다음
그 위에 덮어씀.
문제는 어차피 전부 새로 쓸 데이터라, 이 읽기 자체가 필요 없는 낭비라는 점임.
게다가 나중에 그 dirty 라인이 캐시에서 밀려날 때 또 한 번 메모리로 write-back 됨.
결국 RFO 읽기 1번 + 나중 write-back 1번, 총 두 번의 메모리 트랜잭션이 발생함.
WC는 이 읽기를 아예 생략함
HOST_VISIBLE만 있고 HOST_CACHED는 없는 메모리("CPU→GPU 논캐시 전송" 유형)는
보통 CPU 쪽에서 Write-Combining(WC) 메모리 타입으로 매핑됨.
WC는 코어당 몇 개 안 되는(보통 4~10개) 작은 전용 버퍼를 씀.
각 버퍼 엔트리는 캐시라인 하나(보통 64바이트) 크기의 목표 주소를 담당하고,
그 주소에 들어오는 여러 개의 작은 쓰기를 모았다가(combining)
꽉 차거나 다른 주소로 넘어가는 시점에 한 번의 큰 버스트 쓰기로 메모리에 내보냄.
RFO 없이 곧바로 쓰기만 하니 트랜잭션이 하나 줄고,
쓰고 다시 안 읽을 데이터로 캐시를 오염시켜서 다른 유용한 데이터를 밀어내는 부작용도 없음.
이게 "CPU→GPU 논캐시 전송"이 업로드에 적합하고,
"GPU→CPU 캐시 리드백"은 반대 방향(GPU가 쓰고 CPU가 반복해서 읽는 경우)에만
필요한 이유임.
WC 버퍼가 부족해지는 경우
WC 버퍼 엔트리는 코어당 소수(4~10개)뿐인 고정 하드웨어 자원임.
그래서 동시에 활성화해야 하는 서로 다른 목표 주소의 개수가 이 엔트리 수를 넘으면,
기존 엔트리 중 하나를 (아직 다 안 채워졌어도) 강제로 지금 상태로 flush해서 자리를 비워야 함.
이후 코드가 그 주소로 다시 돌아와 나머지를 쓰려고 하면,
이미 엔트리가 사라진 상태라 완전히 새 엔트리를 배정받고,
같은 캐시라인 주소에 두 번째의 더 작은 버스트 전송을 하게 됨.
원래 한 번의 깔끔한 버스트로 나갔어야 할 게 쪼개져서 나가는 셈이라,
WC의 combining 효과가 무너짐.
구체적 예시: 씬에 오브젝트가 여러 개 있고, 각자의 유니폼 버퍼(MVP 행렬 등)가 VulkanMemory의 TLSF arena 안에서 서로 떨어진 offset에 서브 할당돼 있는 상황.
매 프레임 오브젝트를 순회하며 갱신할 때, 그 순회 순서가 메모리 주소 순서와 일치할 이유가
없어서 여러 개의 멀리 떨어진 영역을 번갈아 쓰게 됨 — WC 엔트리를 스래싱시키는 전형적 패턴임.
이 현상은 캐시 스래싱(작업 집합이 캐시 용량을 넘어서 계속 밀려나는 것)과 같은 종류의 문제고,
메모리 할당기(allocator)의 파편화와는 다른 개념임 — 다만 arena의 서브 할당이
흩어져 있는 것(할당기 파편화)이 원인이 되어, 결과적으로 WC 스래싱을 유발한다는 점에서
인과관계로 이어져 있음.
다이어그램
1. 세 축의 조합 구조
flowchart TD
BT["eBufferType<br/>(역할)"] -->|"GetBufferType()"| U1["VkBufferUsageFlags<br/>역할 비트"]
BTr["eBufferTransfer<br/>(Source/Destination)"] -->|"GetBufferTransfer()"| U2["VkBufferUsageFlags<br/>Transfer 비트"]
U1 --> U["최종 VkBufferUsageFlags<br/>(OR 결합)"]
U2 --> U
DA["eDataAccess<br/>(Static/Dynamic/Stream/Staging)"] -->|"GetBufferProperty()"| P["VkMemoryPropertyFlags"]
BTr -->|"방향(eStaging일 때만 사용)"| P
통합 메모리 공유(CPU/GPU가 같은 RAM과 캐시 일관성 하드웨어를 공유)는
별도 아키텍처 전용이라 위 다이어그램엔 없음.
3. WC 엔트리 스래싱 (파편화된 오브젝트 유니폼 버퍼 갱신)
sequenceDiagram
participant CPU
participant WC as WC 버퍼 풀(엔트리 소수)
participant MEM as 메모리
CPU->>WC: Object1 유니폼 쓰기 (영역 A)
WC->>WC: 엔트리 #1을 A에 배정
CPU->>WC: Object2 유니폼 쓰기 (영역 B, A와 멀리 떨어짐)
WC->>WC: 엔트리 #2를 B에 배정
CPU->>WC: ObjectN+1 유니폼 쓰기 (영역 N+1)
Note over WC: 모든 엔트리 사용 중, 빈 슬롯 없음
WC->>MEM: 가장 오래된 엔트리(#1, 영역 A) 강제 flush<br/>(다 안 채워졌을 수 있음)
WC->>WC: 엔트리 #1을 영역 N+1에 재배정
CPU->>WC: Object1 유니폼 나머지 쓰기 (영역 A로 복귀)
Note over WC: 영역 A용 엔트리가 이미 사라짐
WC->>WC: 영역 A에 새 엔트리 배정
WC->>MEM: 같은 주소로 두 번째의 더 작은 버스트 전송<br/>(combining 효과 상실)
Loading
4. GetBufferProperty 결정 흐름
flowchart TD
Start{"eDataAccess?"}
Start -->|eStatic| A["DEVICE_LOCAL"]
Start -->|eDynamic| B["DEVICE_LOCAL|HOST_VISIBLE|HOST_COHERENT<br/>매핑 공유 우선"]
Start -->|eStream| C["DEVICE_LOCAL|HOST_VISIBLE|HOST_COHERENT<br/>매핑 공유 우선<br/>(큰 경우 InitializeBuffer가 재-staging 전략으로 대체)"]
Start -->|eStaging| Dir{"eBufferTransfer?"}
Dir -->|eSource| D1["HOST_VISIBLE|HOST_COHERENT<br/>(업로드)"]
Dir -->|eDestination| D2["HOST_VISIBLE|HOST_CACHED<br/>(리드백)"]
Vulkan Buffer 메모리 전략 (eBufferType / eBufferTransfer / eDataAccess ↔ Vulkan Flags)
작성자: Winteradio
날짜: 2026-08-09
주제:
VulkanSystem::InitializeBuffer—eBufferType/eBufferTransfer/eDataAccess를 VulkanVkBufferUsageFlags/VkMemoryPropertyFlags로 매핑하는 설계상태: 🟡 설계 확정, 구현 진행 중
배경
VulkanSystem::InitializeBuffer()는 세 개의 독립적인 축을 조합해서 최종 Vulkan 값을 만듦.eBufferType— 이 버퍼가 어느 GPU 파이프라인 단계에 바인딩되는가 →VkBufferUsageFlags의 역할 비트eBufferTransfer— 이 버퍼가 copy 명령의 원본/목적지로 쓰일 수 있는가 →VkBufferUsageFlags의 Transfer 비트eDataAccess+eBufferTransfer— 이 버퍼가 물리적으로 어디에 있어야 하는가 →VkMemoryPropertyFlagseBufferType— 역할 비트어느 GPU 파이프라인 단계(고정 기능 유닛 또는 셰이더 바인딩)가 이 버퍼를 읽는지(또는 storage의 경우 읽고 쓰는지)를 나타냄.
eBufferTypeVkBufferUsageFlagseVertexVERTEX_BUFFER_BITeIndexINDEX_BUFFER_BITeConstUNIFORM_BUFFER_BITeStorageSTORAGE_BUFFER_BITeIndirectINDIRECT_BUFFER_BITeTexelUNIFORM_TEXEL_BUFFER_BIT | STORAGE_TEXEL_BUFFER_BITeNoneeBufferTransfer— Transfer 비트 (copy 엔진 전용, 독립된 축)vkCmdCopyBuffer같은 copy 명령은 vertex-fetch나 셰이더 코어와는 별개의 전송(DMA) 엔진이 처리함.그래서
eBufferType의 역할 비트가 있다고 해서 자동으로 copy 명령에 참여할 수 있는 건 아님 —draw call에서 vertex로 읽히는 것과 copy 명령에서 원본/목적지로 쓰이는 건 서로 무관한 별개의 용도라
각각 명시적으로 비트를 켜야 함.
eBufferTransferVkBufferUsageFlagseSourceTRANSFER_SRC_BITsrcBuffer가 될 수 있음 — 전송 엔진이 여기서 읽어감eDestinationTRANSFER_DST_BITdstBuffer가 될 수 있음 — 전송 엔진이 여기로 써줌비트플래그라
eSource | eDestination동시 설정도 가능함(스크래치/중간 버퍼 등).결합 예시
VkBufferUsageFlags = GetBufferType(bufferType) | GetBufferTransfer(bufferTransfer)eBufferTypeeBufferTransfereNoneeSourceTRANSFER_SRC_BITeNoneeDestinationTRANSFER_DST_BITeVertexeNoneVERTEX_BUFFER_BITeStatic— 업로드 후 다시 안 건드림eVertexeDestinationVERTEX_BUFFER_BIT | TRANSFER_DST_BITeDynamic/eStream(큰 경우) — copy로 재-stagingeVertexeSourceVERTEX_BUFFER_BIT | TRANSFER_SRC_BITeDataAccess+eBufferTransfer→VkMemoryPropertyFlags(GetBufferProperty)eDataAccess::eStaging는 "GPU 바인딩 역할이 없는 순수 전송 버퍼"를 뜻함(주석 참고: GPU Binding Datas는eStatic/eDynamic/eStream, GPU Transfer Datas는eStaging). 이 값과eBufferTransfer의 방향을조합하면 업로드용인지 리드백용인지가 구분됨 — 별도로
eReadBack값을 추가하지 않아도 됨.eDataAccesseBufferTransferVkMemoryPropertyFlagseStaticDEVICE_LOCALeDynamicDEVICE_LOCAL|HOST_VISIBLE|HOST_COHERENTeStreamDEVICE_LOCAL|HOST_VISIBLE|HOST_COHERENTInitializeBuffer가 별도 처리)eStagingeSourceHOST_VISIBLE|HOST_COHERENTeStagingeDestinationHOST_VISIBLE|HOST_CACHEDeStatic/eDynamic/eStream은 항상 GPU에 바인딩되는 "진짜" 리소스라eBufferTransfer방향과 무관하게값이 고정됨.
eStaging일 때만 방향을 봐서 COHERENT(업로드)냐 CACHED(리드백)냐를 가름.eDynamic/eStream의 "매핑 공유 우선 시도"는DEVICE_LOCAL|HOST_VISIBLE|HOST_COHERENT조합이 용량제한으로 실패할 수 있다는 뜻 — 그때는
HOST_VISIBLE|HOST_COHERENT만으로 재시도해야 함. 이 폴백은GetBufferProperty하나의 반환값으로 표현할 수 없는 실제 할당 시도 로직이라InitializeBuffer가m_memory->Allocate()결과를 보고 처리함.왜 생성 시점에 별도의 매핑 의도 플래그가 없는가
Unreal Engine의
EBufferUsageFlags도BUF_Static/BUF_Dynamic/BUF_Volatile을 생성 시점에 한 번 정하고,그 값 자체가 곧 매핑 가능 여부를 결정함 — 별도로 "나중에 매핑할 거야?"를 또 묻지 않음.
같은 이유로
eDataAccess::eDynamic/eStream이라는 값 자체가 이미 "CPU가 이후에 이 버퍼를 건드릴 것"이라는의도를 담고 있어서, 생성 시점에 별도의 매핑 의도 신호가 필요 없음. CPU가 생성 후 전혀 안 건드릴 거면
그건 애초에
eStatic이었어야 함. (참고로eMapAccess는RHIBufferUpdateDesc에만 있고, 이건 매 Lock/Map호출마다 정확히 뭘 할 건지를 나타내는 별개의 용도 — GL의
glMapBufferRangeaccess 파라미터와 같은 자리.)VkBufferCreateInfo필드sTypeVK_STRUCTURE_TYPE_BUFFER_CREATE_INFOpNextnullptrflags0sizeVkMemoryRequirements::size)는 정렬 때문에 이보다 커질 수 있음usageGetBufferType(bufferType) | GetBufferTransfer(bufferTransfer)sharingModeVK_SHARING_MODE_EXCLUSIVEVulkanDevice가 GRAPHICS|COMPUTE|TRANSFER를 만족하는 큐 패밀리 하나만 쓰므로, 여러 큐 패밀리가 나눠 쓰는 상황 자체가 없음queueFamilyIndexCount0EXCLUSIVE일 때 무시됨pQueueFamilyIndicesnullptrEXCLUSIVE일 때 무시됨메모리 속성 유형 — 하드웨어 데이터 전송 경로별 분류
eStatic/eDynamic/eStream/eStaging에 적용되는 다섯 가지 전송 경로.DEVICE_LOCAL만HOST_VISIBLE | HOST_COHERENTHOST_VISIBLE | HOST_CACHEDDEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENTHOST_VISIBLE | HOST_COHERENT | HOST_CACHED핵심 규칙 두 가지
1.
HOST_COHERENT는HOST_VISIBLE없이 단독으로 쓰지 않음.COHERENT는 CPU가 매핑해서 접근할 때의 캐시 동작을 설명하는 플래그임.
매핑 자체가 안 되는 메모리엔 적용 대상이 없음.
2.
DEVICE_LOCAL과HOST_VISIBLE은 공존 가능하지만 용량이 제한적임(GPU-CPU 매핑 공유).그래서 "작으면 CPU 쪽, 크면 GPU 쪽"이 아니라 반대에 가까움.
작은 버퍼가 오히려 이 좁은 풀에 들어가서 GPU 속도와 CPU 접근을 동시에 얻고,
큰 버퍼는 이 풀에 못 들어가서 순수
HOST_VISIBLE로 떨어짐.구현상으로는 "매핑 공유를 먼저 시도하고, 실패하면 논캐시 전송으로 폴백"하는 2단계 로직임.
Write-Combining(WC) — 왜 CPU 쓰기 전용 데이터는 캐시가 없는 편이 더 빠른가
한 번 쓰고 CPU가 다시 읽지 않는 데이터(vertex/uniform buffer 업로드)라면,
일반 캐시를 거치는 경로가 오히려 더 비쌈.
RFO가 낭비인 이유
일반 write-back 캐시는 특정 캐시라인에 쓰기를 하려면,
그 라인을 캐시에 온전히 올려야 dirty 표시를 할 수 있음.
아직 캐시에 없는 라인이면 CPU는 먼저 그 라인을 메모리에서 읽어와서(RFO) 캐시에 올린 다음
그 위에 덮어씀.
문제는 어차피 전부 새로 쓸 데이터라, 이 읽기 자체가 필요 없는 낭비라는 점임.
게다가 나중에 그 dirty 라인이 캐시에서 밀려날 때 또 한 번 메모리로 write-back 됨.
결국 RFO 읽기 1번 + 나중 write-back 1번, 총 두 번의 메모리 트랜잭션이 발생함.
WC는 이 읽기를 아예 생략함
HOST_VISIBLE만 있고HOST_CACHED는 없는 메모리("CPU→GPU 논캐시 전송" 유형)는보통 CPU 쪽에서 Write-Combining(WC) 메모리 타입으로 매핑됨.
WC는 코어당 몇 개 안 되는(보통 4~10개) 작은 전용 버퍼를 씀.
각 버퍼 엔트리는 캐시라인 하나(보통 64바이트) 크기의 목표 주소를 담당하고,
그 주소에 들어오는 여러 개의 작은 쓰기를 모았다가(combining)
꽉 차거나 다른 주소로 넘어가는 시점에 한 번의 큰 버스트 쓰기로 메모리에 내보냄.
RFO 없이 곧바로 쓰기만 하니 트랜잭션이 하나 줄고,
쓰고 다시 안 읽을 데이터로 캐시를 오염시켜서 다른 유용한 데이터를 밀어내는 부작용도 없음.
이게 "CPU→GPU 논캐시 전송"이 업로드에 적합하고,
"GPU→CPU 캐시 리드백"은 반대 방향(GPU가 쓰고 CPU가 반복해서 읽는 경우)에만
필요한 이유임.
WC 버퍼가 부족해지는 경우
WC 버퍼 엔트리는 코어당 소수(4~10개)뿐인 고정 하드웨어 자원임.
그래서 동시에 활성화해야 하는 서로 다른 목표 주소의 개수가 이 엔트리 수를 넘으면,
기존 엔트리 중 하나를 (아직 다 안 채워졌어도) 강제로 지금 상태로 flush해서 자리를 비워야 함.
이후 코드가 그 주소로 다시 돌아와 나머지를 쓰려고 하면,
이미 엔트리가 사라진 상태라 완전히 새 엔트리를 배정받고,
같은 캐시라인 주소에 두 번째의 더 작은 버스트 전송을 하게 됨.
원래 한 번의 깔끔한 버스트로 나갔어야 할 게 쪼개져서 나가는 셈이라,
WC의 combining 효과가 무너짐.
구체적 예시: 씬에 오브젝트가 여러 개 있고, 각자의 유니폼 버퍼(MVP 행렬 등)가
VulkanMemory의 TLSF arena 안에서 서로 떨어진 offset에 서브 할당돼 있는 상황.매 프레임 오브젝트를 순회하며 갱신할 때, 그 순회 순서가 메모리 주소 순서와 일치할 이유가
없어서 여러 개의 멀리 떨어진 영역을 번갈아 쓰게 됨 — WC 엔트리를 스래싱시키는 전형적 패턴임.
이 현상은 캐시 스래싱(작업 집합이 캐시 용량을 넘어서 계속 밀려나는 것)과 같은 종류의 문제고,
메모리 할당기(allocator)의 파편화와는 다른 개념임 — 다만 arena의 서브 할당이
흩어져 있는 것(할당기 파편화)이 원인이 되어, 결과적으로 WC 스래싱을 유발한다는 점에서
인과관계로 이어져 있음.
다이어그램
1. 세 축의 조합 구조
flowchart TD BT["eBufferType<br/>(역할)"] -->|"GetBufferType()"| U1["VkBufferUsageFlags<br/>역할 비트"] BTr["eBufferTransfer<br/>(Source/Destination)"] -->|"GetBufferTransfer()"| U2["VkBufferUsageFlags<br/>Transfer 비트"] U1 --> U["최종 VkBufferUsageFlags<br/>(OR 결합)"] U2 --> U DA["eDataAccess<br/>(Static/Dynamic/Stream/Staging)"] -->|"GetBufferProperty()"| P["VkMemoryPropertyFlags"] BTr -->|"방향(eStaging일 때만 사용)"| P2. 전송 경로별 하드웨어 배치
flowchart LR Cache["CPU Cache<br/>L1/L2/L3"] RAM["System RAM"] VramFull["VRAM<br/>GPU 전용 메모리"] VramSmall["VRAM<br/>GPU-CPU 매핑 공유(용량 제한)"] GPU["GPU Cores"] Cache -->|"GPU→CPU 캐시 리드백<br/>invalidate 필요"| RAM RAM -.->|"staging 경유 복사"| VramFull RAM -->|"CPU→GPU 논캐시 전송<br/>GPU가 PCIe로 읽음"| VramFull VramSmall -->|"GPU-CPU 매핑 공유<br/>용량 제한"| Cache VramFull --> GPU3. WC 엔트리 스래싱 (파편화된 오브젝트 유니폼 버퍼 갱신)
sequenceDiagram participant CPU participant WC as WC 버퍼 풀(엔트리 소수) participant MEM as 메모리 CPU->>WC: Object1 유니폼 쓰기 (영역 A) WC->>WC: 엔트리 #1을 A에 배정 CPU->>WC: Object2 유니폼 쓰기 (영역 B, A와 멀리 떨어짐) WC->>WC: 엔트리 #2를 B에 배정 CPU->>WC: ObjectN+1 유니폼 쓰기 (영역 N+1) Note over WC: 모든 엔트리 사용 중, 빈 슬롯 없음 WC->>MEM: 가장 오래된 엔트리(#1, 영역 A) 강제 flush<br/>(다 안 채워졌을 수 있음) WC->>WC: 엔트리 #1을 영역 N+1에 재배정 CPU->>WC: Object1 유니폼 나머지 쓰기 (영역 A로 복귀) Note over WC: 영역 A용 엔트리가 이미 사라짐 WC->>WC: 영역 A에 새 엔트리 배정 WC->>MEM: 같은 주소로 두 번째의 더 작은 버스트 전송<br/>(combining 효과 상실)4.
GetBufferProperty결정 흐름flowchart TD Start{"eDataAccess?"} Start -->|eStatic| A["DEVICE_LOCAL"] Start -->|eDynamic| B["DEVICE_LOCAL|HOST_VISIBLE|HOST_COHERENT<br/>매핑 공유 우선"] Start -->|eStream| C["DEVICE_LOCAL|HOST_VISIBLE|HOST_COHERENT<br/>매핑 공유 우선<br/>(큰 경우 InitializeBuffer가 재-staging 전략으로 대체)"] Start -->|eStaging| Dir{"eBufferTransfer?"} Dir -->|eSource| D1["HOST_VISIBLE|HOST_COHERENT<br/>(업로드)"] Dir -->|eDestination| D2["HOST_VISIBLE|HOST_CACHED<br/>(리드백)"]