From 4f8d079d297186ba05d88f784293dfc8c635a7a0 Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Sun, 29 Mar 2026 09:45:31 -0400 Subject: [PATCH] Port hakuX Vulkan memory budget and surface pool --- hw/xbox/nv2a/pgraph/vk/buffer.c | 104 +++++++++++++++-- hw/xbox/nv2a/pgraph/vk/renderer.h | 20 ++++ hw/xbox/nv2a/pgraph/vk/shaders.c | 14 ++- hw/xbox/nv2a/pgraph/vk/surface.c | 187 +++++++++++++++++++++++++----- 4 files changed, 286 insertions(+), 39 deletions(-) diff --git a/hw/xbox/nv2a/pgraph/vk/buffer.c b/hw/xbox/nv2a/pgraph/vk/buffer.c index f8b7bf35b5..6bc89ff8ea 100644 --- a/hw/xbox/nv2a/pgraph/vk/buffer.c +++ b/hw/xbox/nv2a/pgraph/vk/buffer.c @@ -23,6 +23,76 @@ #include #endif +typedef struct MemoryBudget { + size_t total_heap; + size_t renderer_budget; + size_t vertex_inline_cap; + size_t index_cap; + size_t staging_cap; + size_t shader_module_cache_entries; +} MemoryBudget; + +static MemoryBudget compute_memory_budget(PGRAPHVkState *r) +{ + const size_t mib = 1024 * 1024; + const size_t gib = 1024 * mib; + + VkPhysicalDeviceMemoryProperties const *props; + vmaGetMemoryProperties(r->allocator, &props); + + size_t total_heap = 0; + for (uint32_t i = 0; i < props->memoryHeapCount; i++) { + if (props->memoryHeaps[i].size > total_heap) { + total_heap = props->memoryHeaps[i].size; + } + } + + MemoryBudget b = { .total_heap = total_heap }; + +#ifdef __ANDROID__ + if (total_heap <= 4 * gib) { + b.renderer_budget = 512 * mib; + } else if (total_heap <= 6 * gib) { + b.renderer_budget = 768 * mib; + } else if (total_heap <= 8 * gib) { + b.renderer_budget = 1024 * mib; + } else if (total_heap <= 12 * gib) { + b.renderer_budget = 1536 * mib; + } else if (total_heap <= 16 * gib) { + b.renderer_budget = 2048 * mib; + } else if (total_heap <= 22 * gib) { + b.renderer_budget = 3072 * mib; + } else { + b.renderer_budget = SIZE_MAX; + } +#else + b.renderer_budget = SIZE_MAX; +#endif + + if (b.renderer_budget == SIZE_MAX) { + b.vertex_inline_cap = SIZE_MAX; + b.index_cap = SIZE_MAX; + b.staging_cap = SIZE_MAX; + b.shader_module_cache_entries = 50 * 1024; + } else { + size_t budget = b.renderer_budget; + size_t budget_mib = budget / mib; + + b.vertex_inline_cap = MAX(8 * mib, budget / 5); + b.index_cap = MAX(4 * mib, budget / 20); + b.staging_cap = MAX(16 * mib, budget * 12 / 100); + b.shader_module_cache_entries = budget_mib * 4; + if (b.shader_module_cache_entries < 2048) { + b.shader_module_cache_entries = 2048; + } + if (b.shader_module_cache_entries > 50 * 1024) { + b.shader_module_cache_entries = 50 * 1024; + } + } + + return b; +} + static const char *const buffer_names[BUFFER_COUNT] = { "BUFFER_STAGING_DST", "BUFFER_STAGING_SRC", @@ -76,14 +146,14 @@ bool pgraph_vk_init_buffers(NV2AState *d, Error **errp) PGRAPHState *pg = &d->pgraph; PGRAPHVkState *r = pg->vk_renderer_state; - // FIXME: Profile buffer sizes - const size_t mib = 1024 * 1024; size_t vram_size = memory_region_size(d->vram); + MemoryBudget mb = compute_memory_budget(r); size_t staging_size = vram_size; if (staging_size < (16 * mib)) { staging_size = 16 * mib; } + staging_size = MIN(staging_size, mb.staging_cap); size_t compute_size = vram_size * 2; if (compute_size < (64 * mib)) { compute_size = 64 * mib; @@ -98,7 +168,28 @@ bool pgraph_vk_init_buffers(NV2AState *d, Error **errp) } #endif + size_t index_size = sizeof(pg->inline_elements) * 100; + index_size = MIN(index_size, mb.index_cap); + + size_t vertex_inline_size = NV2A_VERTEXSHADER_ATTRIBUTES * + NV2A_MAX_BATCH_LENGTH * + 4 * sizeof(float) * 10; + vertex_inline_size = MIN(vertex_inline_size, mb.vertex_inline_cap); + + r->shader_module_cache_target = mb.shader_module_cache_entries; + #ifdef __ANDROID__ + __android_log_print(ANDROID_LOG_INFO, "xemu-android", + "vk memory budget: heap=%zuMB budget=%s%zuMB staging_cap=%zuMB index_cap=%zuMB vtx_inline_cap=%zuMB shader_cache=%zu", + mb.total_heap >> 20, + mb.renderer_budget == SIZE_MAX ? "uncapped/" : "", + mb.renderer_budget == SIZE_MAX ? 0 : + mb.renderer_budget >> 20, + mb.staging_cap == SIZE_MAX ? 0 : mb.staging_cap >> 20, + mb.index_cap == SIZE_MAX ? 0 : mb.index_cap >> 20, + mb.vertex_inline_cap == SIZE_MAX ? 0 : + mb.vertex_inline_cap >> 20, + mb.shader_module_cache_entries); __android_log_print(ANDROID_LOG_INFO, "xemu-android", "vk buffer init: vram=%zu staging=%zu compute=%zu", vram_size, staging_size, compute_size); @@ -143,13 +234,13 @@ bool pgraph_vk_init_buffers(NV2AState *d, Error **errp) .alloc_info = device_alloc_create_info, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, - .buffer_size = sizeof(pg->inline_elements) * 100, + .buffer_size = index_size, }; r->storage_buffers[BUFFER_INDEX_STAGING] = (StorageBuffer){ .alloc_info = host_alloc_create_info, .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - .buffer_size = r->storage_buffers[BUFFER_INDEX].buffer_size, + .buffer_size = index_size, }; // FIXME: Don't assume that we can render with host mapped buffer @@ -171,14 +262,13 @@ bool pgraph_vk_init_buffers(NV2AState *d, Error **errp) .alloc_info = device_alloc_create_info, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, - .buffer_size = NV2A_VERTEXSHADER_ATTRIBUTES * NV2A_MAX_BATCH_LENGTH * - 4 * sizeof(float) * 10, + .buffer_size = vertex_inline_size, }; r->storage_buffers[BUFFER_VERTEX_INLINE_STAGING] = (StorageBuffer){ .alloc_info = host_alloc_create_info, .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - .buffer_size = r->storage_buffers[BUFFER_VERTEX_INLINE].buffer_size, + .buffer_size = vertex_inline_size, }; r->storage_buffers[BUFFER_UNIFORM] = (StorageBuffer){ diff --git a/hw/xbox/nv2a/pgraph/vk/renderer.h b/hw/xbox/nv2a/pgraph/vk/renderer.h index 279dc04b8d..b261737de7 100644 --- a/hw/xbox/nv2a/pgraph/vk/renderer.h +++ b/hw/xbox/nv2a/pgraph/vk/renderer.h @@ -148,6 +148,21 @@ typedef struct SurfaceBinding { bool initialized; } SurfaceBinding; +typedef struct SurfaceImageConfig { + VkFormat format; + uint32_t width, height; + VkImageUsageFlags usage; +} SurfaceImageConfig; + +typedef struct PooledSurfaceImage { + QTAILQ_ENTRY(PooledSurfaceImage) entry; + SurfaceImageConfig config; + VkImage image; + VmaAllocation allocation; + VkImage image_scratch; + VmaAllocation allocation_scratch; +} PooledSurfaceImage; + typedef struct ShaderModuleInfo { int refcnt; char *glsl; @@ -393,6 +408,8 @@ typedef struct PGRAPHVkState { QTAILQ_HEAD(, SurfaceBinding) surfaces; QTAILQ_HEAD(, SurfaceBinding) invalid_surfaces; + QTAILQ_HEAD(, PooledSurfaceImage) surface_image_pool; + int surface_image_pool_count; SurfaceBinding *color_binding, *zeta_binding; bool downloads_pending; QemuEvent downloads_complete; @@ -415,6 +432,7 @@ typedef struct PGRAPHVkState { Lru shader_module_cache; ShaderModuleCacheEntry *shader_module_cache_entries; + size_t shader_module_cache_target; // FIXME: Merge these into a structure uint64_t uniform_buffer_hashes[2]; @@ -516,6 +534,8 @@ VkDeviceSize pgraph_vk_update_vertex_inline_buffer(PGRAPHState *pg, void **data, void pgraph_vk_init_surfaces(PGRAPHState *pg); void pgraph_vk_finalize_surfaces(PGRAPHState *pg); void pgraph_vk_surface_flush(NV2AState *d); +void pgraph_vk_surface_image_pool_init(PGRAPHVkState *r); +void pgraph_vk_surface_image_pool_drain(PGRAPHVkState *r); void pgraph_vk_process_pending_downloads(NV2AState *d); void pgraph_vk_surface_download_if_dirty(NV2AState *d, SurfaceBinding *surface); SurfaceBinding *pgraph_vk_surface_get_within(NV2AState *d, hwaddr addr); diff --git a/hw/xbox/nv2a/pgraph/vk/shaders.c b/hw/xbox/nv2a/pgraph/vk/shaders.c index ea9beeb5e7..e936d25f6e 100644 --- a/hw/xbox/nv2a/pgraph/vk/shaders.c +++ b/hw/xbox/nv2a/pgraph/vk/shaders.c @@ -385,9 +385,17 @@ static void shader_cache_init(PGRAPHState *pg) r->shader_cache.compare_nodes = shader_cache_entry_compare; r->shader_cache.post_node_evict = shader_cache_entry_post_evict; - /* FIXME: Make this configurable */ - const size_t shader_module_cache_size = 50 * 1024; - lru_init(&r->shader_module_cache, 1 << 16); + const size_t shader_module_cache_size = + r->shader_module_cache_target ? r->shader_module_cache_target + : 50 * 1024; + size_t shader_module_hash_buckets = shader_module_cache_size * 2; + if (shader_module_hash_buckets < 4096) { + shader_module_hash_buckets = 4096; + } + if (shader_module_hash_buckets > (1 << 16)) { + shader_module_hash_buckets = 1 << 16; + } + lru_init(&r->shader_module_cache, shader_module_hash_buckets); r->shader_module_cache_entries = g_malloc_n(shader_module_cache_size, sizeof(ShaderModuleCacheEntry)); assert(r->shader_module_cache_entries != NULL); diff --git a/hw/xbox/nv2a/pgraph/vk/surface.c b/hw/xbox/nv2a/pgraph/vk/surface.c index 256d253c12..393f4d512d 100644 --- a/hw/xbox/nv2a/pgraph/vk/surface.c +++ b/hw/xbox/nv2a/pgraph/vk/surface.c @@ -775,6 +775,87 @@ static void set_surface_label(PGRAPHState *pg, SurfaceBinding const *surface) } } +#define SURFACE_IMAGE_POOL_MAX_SIZE 64 + +void pgraph_vk_surface_image_pool_init(PGRAPHVkState *r) +{ + QTAILQ_INIT(&r->surface_image_pool); + r->surface_image_pool_count = 0; +} + +static bool surface_image_pool_config_match(const SurfaceImageConfig *a, + const SurfaceImageConfig *b) +{ + return a->format == b->format && + a->width == b->width && + a->height == b->height && + a->usage == b->usage; +} + +static bool surface_image_pool_acquire(PGRAPHVkState *r, + const SurfaceImageConfig *config, + VkImage *out_image, + VmaAllocation *out_alloc, + VkImage *out_scratch, + VmaAllocation *out_scratch_alloc) +{ + PooledSurfaceImage *entry; + QTAILQ_FOREACH(entry, &r->surface_image_pool, entry) { + if (surface_image_pool_config_match(&entry->config, config)) { + *out_image = entry->image; + *out_alloc = entry->allocation; + *out_scratch = entry->image_scratch; + *out_scratch_alloc = entry->allocation_scratch; + QTAILQ_REMOVE(&r->surface_image_pool, entry, entry); + g_free(entry); + r->surface_image_pool_count--; + return true; + } + } + return false; +} + +static void surface_image_pool_release(PGRAPHVkState *r, + const SurfaceImageConfig *config, + VkImage image, + VmaAllocation allocation, + VkImage image_scratch, + VmaAllocation allocation_scratch) +{ + if (r->surface_image_pool_count >= SURFACE_IMAGE_POOL_MAX_SIZE) { + PooledSurfaceImage *oldest = QTAILQ_FIRST(&r->surface_image_pool); + assert(oldest != NULL); + QTAILQ_REMOVE(&r->surface_image_pool, oldest, entry); + vmaDestroyImage(r->allocator, oldest->image, oldest->allocation); + vmaDestroyImage(r->allocator, oldest->image_scratch, + oldest->allocation_scratch); + g_free(oldest); + r->surface_image_pool_count--; + } + + PooledSurfaceImage *entry = g_malloc(sizeof(PooledSurfaceImage)); + entry->config = *config; + entry->image = image; + entry->allocation = allocation; + entry->image_scratch = image_scratch; + entry->allocation_scratch = allocation_scratch; + QTAILQ_INSERT_TAIL(&r->surface_image_pool, entry, entry); + r->surface_image_pool_count++; +} + +void pgraph_vk_surface_image_pool_drain(PGRAPHVkState *r) +{ + PooledSurfaceImage *entry, *next; + QTAILQ_FOREACH_SAFE(entry, &r->surface_image_pool, entry, next) { + QTAILQ_REMOVE(&r->surface_image_pool, entry, entry); + vmaDestroyImage(r->allocator, entry->image, entry->allocation); + vmaDestroyImage(r->allocator, entry->image_scratch, + entry->allocation_scratch); + g_free(entry); + } + r->surface_image_pool_count = 0; +} + static void create_surface_image(PGRAPHState *pg, SurfaceBinding *surface) { PGRAPHVkState *r = pg->vk_renderer_state; @@ -790,36 +871,66 @@ static void create_surface_image(PGRAPHState *pg, SurfaceBinding *surface) "Creating new surface image width=%d height=%d @ %08" HWADDR_PRIx, width, height, surface->vram_addr); - VkImageCreateInfo image_create_info = { - .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - .imageType = VK_IMAGE_TYPE_2D, - .extent.width = width, - .extent.height = height, - .extent.depth = 1, - .mipLevels = 1, - .arrayLayers = 1, + VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + surface->host_fmt.usage; + + SurfaceImageConfig pool_cfg = { .format = surface->host_fmt.vk_format, - .tiling = VK_IMAGE_TILING_OPTIMAL, - .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, - .usage = VK_IMAGE_USAGE_SAMPLED_BIT | - VK_IMAGE_USAGE_TRANSFER_DST_BIT | - VK_IMAGE_USAGE_TRANSFER_SRC_BIT | surface->host_fmt.usage, - .samples = VK_SAMPLE_COUNT_1_BIT, - .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .width = width, + .height = height, + .usage = usage, }; - VmaAllocationCreateInfo alloc_create_info = { - .usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE, - }; + if (surface_image_pool_acquire(r, &pool_cfg, + &surface->image, &surface->allocation, + &surface->image_scratch, + &surface->allocation_scratch)) { + surface->image_scratch_current_layout = VK_IMAGE_LAYOUT_UNDEFINED; + } else { + VkImageCreateInfo image_create_info = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .extent.width = width, + .extent.height = height, + .extent.depth = 1, + .mipLevels = 1, + .arrayLayers = 1, + .format = surface->host_fmt.vk_format, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .usage = usage, + .samples = VK_SAMPLE_COUNT_1_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + }; - VK_CHECK(vmaCreateImage(r->allocator, &image_create_info, - &alloc_create_info, &surface->image, - &surface->allocation, NULL)); + VmaAllocationCreateInfo alloc_create_info = { + .usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE, + }; - VK_CHECK(vmaCreateImage(r->allocator, &image_create_info, - &alloc_create_info, &surface->image_scratch, - &surface->allocation_scratch, NULL)); - surface->image_scratch_current_layout = VK_IMAGE_LAYOUT_UNDEFINED; + VkResult res = vmaCreateImage(r->allocator, &image_create_info, + &alloc_create_info, &surface->image, + &surface->allocation, NULL); + if (res != VK_SUCCESS) { + pgraph_vk_surface_image_pool_drain(r); + VK_CHECK(vmaCreateImage(r->allocator, &image_create_info, + &alloc_create_info, &surface->image, + &surface->allocation, NULL)); + } + + res = vmaCreateImage(r->allocator, &image_create_info, + &alloc_create_info, &surface->image_scratch, + &surface->allocation_scratch, NULL); + if (res != VK_SUCCESS) { + pgraph_vk_surface_image_pool_drain(r); + VK_CHECK(vmaCreateImage(r->allocator, &image_create_info, + &alloc_create_info, + &surface->image_scratch, + &surface->allocation_scratch, NULL)); + } + surface->image_scratch_current_layout = VK_IMAGE_LAYOUT_UNDEFINED; + } VkImageViewCreateInfo image_view_create_info = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, @@ -871,12 +982,28 @@ static void destroy_surface_image(PGRAPHVkState *r, SurfaceBinding *surface) vkDestroyImageView(r->device, surface->image_view, NULL); surface->image_view = VK_NULL_HANDLE; - vmaDestroyImage(r->allocator, surface->image, surface->allocation); + unsigned int width = surface->width ? surface->width : 1; + unsigned int height = surface->height ? surface->height : 1; + unsigned int scale_factor = g_nv2a->pgraph.surface_scale_factor; + if (scale_factor > 1) { + width *= scale_factor; + height *= scale_factor; + } + SurfaceImageConfig pool_cfg = { + .format = surface->host_fmt.vk_format, + .width = width, + .height = height, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | surface->host_fmt.usage, + }; + surface_image_pool_release(r, &pool_cfg, + surface->image, surface->allocation, + surface->image_scratch, + surface->allocation_scratch); + surface->image = VK_NULL_HANDLE; surface->allocation = VK_NULL_HANDLE; - - vmaDestroyImage(r->allocator, surface->image_scratch, - surface->allocation_scratch); surface->image_scratch = VK_NULL_HANDLE; surface->allocation_scratch = VK_NULL_HANDLE; } @@ -1748,6 +1875,7 @@ void pgraph_vk_init_surfaces(PGRAPHState *pg) QTAILQ_INIT(&r->surfaces); QTAILQ_INIT(&r->invalid_surfaces); + pgraph_vk_surface_image_pool_init(r); r->downloads_pending = false; qemu_event_init(&r->downloads_complete, false); @@ -1785,6 +1913,7 @@ void pgraph_vk_surface_flush(NV2AState *d) invalidate_surface(d, s); } prune_invalid_surfaces(r, 0); + pgraph_vk_surface_image_pool_drain(r); pgraph_vk_reload_surface_scale_factor(pg); }