Files
Valiantsin DzerakhandClaude Opus 5.5 cf44e9013c Merge task/memoom: wait out a short out-of-memory spell instead of ending the game
Conflicts: the image retry goes through vk_texture_image, sweep_pressure keeps the released-texture cap on the new
drop helpers, and vk_headroom_release also frees the texture pool's anchor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:12:55 +03:00

2605 lines
135 KiB
C

// Vulkan textures of the graphics layer: a VkImage cache keyed by storage
// GPU address plus descriptor, uploads through staging buffers with block-linear deswizzle and ASTC decode (vk_astc.c),
// re-uploads when page write generations (vk_memory.c) show the storage written, ReadTexels / WriteTexels, the texture
// and sampler descriptor slots, and VkSamplers.
//
// Behaviour ported from MacNativeGames games/totk/third_party/d2r_gfx/gfx/nvn @ b75be8cf (MIT, Copyright (c) 2026
// jhaizhou; origin d2r-mobile): which textures mirror their pool memory (nvn_mtl.m tex_mirrored, 3347-3362), the
// "GPU-owned" rule that stops re-uploads once the GPU rendered into a mirrored texture (nvn_mtl.m tex_mtl, 3366-3372),
// the live texture at a storage address that a slot of a finalized texture follows (hle_nvn.c:373-393), and
// ReadTexels / WriteTexels (nvn_mtl.m:10105-10195). Image creation, upload regions and samplers are ported from LAS
// Recomp runtime/nvn/vulkan/textures.c:235-330, 778-844 @ da35b83a (MIT, Copyright (c) 2026 Peiikai).
//
// Every image lives in VK_IMAGE_LAYOUT_GENERAL. Uploads are recorded into the upload batch of vk_memory.c; the replay
// submits it (vk_upload_flush) before a submission that samples the textures.
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "gfx.h"
#include "host.h"
#include "nvn_api.h"
#include "nvn_hmap.h"
#include "nvn_tex.h"
#include "tex.h"
#include "vk.h"
#include "vk_dlaa.h"
#include "vk_exec.h"
#include "platform/platform.h"
typedef struct {
uint64_t addr;
nvn_texdesc d;
uint8_t has_layout, layout[8], pad[7];
} TexKey;
typedef struct Tex {
VkTexImage pub; // first: vk_texture_get hands out &pub
TexKey key;
tex_t t; // the newest texture object seen with this key
TexVkFormat f;
VmaAllocation alloc; // own memory; NULL when bound into pool memory
VmaAllocation bound_to; // the pool memory it is bound into
uint32_t wgen; // page generation of the last upload check
uint32_t checked; // submit_epoch of the last check from a draw binding
int uploaded, released;
uint32_t rev_epoch, rev_pos; // the state scan made it live in that submission at that op (vk_texture_revive)
uint32_t own_epoch, own_pos; // a chunk made it GPU-owned in that submission at that op (vk_texture_gpu_written)
uint32_t n_up; // uploads of this entry (vk_texture_debug)
int last_ch; // last vk_mem_changed result of make_fresh: 1 written, 0 clean, -1 not tracked
struct Tex *next_at; // other entries at the same storage address
uint8_t gpu_dirty; // mirrored, and the GPU wrote the image after the last write-back to the storage
uint32_t gpu_gen; // page generation when the GPU write that set gpu_dirty was recorded
uint64_t gpu_seq; // order of that GPU write among all of them (gpu_writes)
uint64_t gpu_fence; // fence of the submission that made that write (0: not recorded in one)
uint64_t run_frame; // mirrored: the frame of the newest GPU write
uint8_t run; // frames in a row up to run_frame with a GPU write, counted up to WB_RUN
uint64_t wr_fence; // any entry: newest submission recorded to write its image (render, copy, clear,
// storage binding); ReadTexels waits for it instead of draining the replay queue
uint8_t listed; // in owned[]
uint8_t *snap; // a texture that fills no page: its storage bytes when the GPU write was recorded
uint32_t wt_n; // WriteTexels into the storage since the last upload
struct Tex *rel_prev, *rel_next; // while released: the list of released entries, oldest first
uint64_t rel_ms; // while released: when (coarse_ms)
uint64_t rel_bytes; // while released: its own memory, counted in rel_bytes_total
uint64_t use_seq; // order of the last creation or take-back among all entries
uint64_t rel_seq; // while released: use_seq of the newest entry placed before this release; an entry
// with a later use_seq on overlapping storage replaced this one's bytes
} Tex;
// A slot keeps what vk_texture_by_id resolved for it, so a draw binding skips the key lookup and the address chain: the
// entry (ce), valid while the generation of its address (agen) is still cgen, the view and has_view handed out with it,
// and the submit_epoch of the last freshness check through the slot (cepoch). Hot fields first: a lookup that hits
// touches one or two cache lines.
typedef struct {
struct Tex *ce; // NULL: nothing cached (registered since, or never resolved)
uint32_t cgen; // agen of the slot's address when ce was resolved
uint32_t cepoch;
view_t cview;
uint8_t chas_view, used, has_view;
view_t v;
tex_t t;
} TexSlot;
typedef struct { samp_par p; VkSampler s, sp; uint8_t used; } SampSlot; // s: vk_sampler_for(&p) once looked up; sp:
// its point-sampled form (vk_sampler_point_by_id_locked)
static PlatRMutex tex_lock;
static PlatOnce tex_once = PLAT_ONCE_INIT;
static nvn_hmap by_key = {.klen = sizeof(TexKey)};
static nvn_hmap by_addr = {.klen = 8};
static TexSlot *tslots;
static int ntslots;
static SampSlot *sslots;
static int nsslots;
static nvn_hmap samplers = {.klen = sizeof(samp_par)};
static int nreleased;
static uint64_t rel_bytes_total; // own memory of the released entries
static Tex *rel_head, *rel_tail; // released entries in release order (sweep_released drops from the head)
static uint64_t n_uploads, n_reuploads;
// Re-uploads since the last perf line (tex_lock): all, and those of textures a WriteTexels wrote since their last upload.
static uint64_t ru_n, ru_bytes, ru_ns, ru_wt_n, ru_wt_bytes, ru_wt_ns;
// TOTK_TEX_ALIAS (default on; =0: a new image over live storage starts empty): a new descriptor over the storage of a
// live image that the GPU wrote gets that image's content (alias_copy). Counts since the last log line.
static int tex_alias = -1;
static uint64_t n_alias_copied, n_alias_upload, n_alias_uninit;
static uint32_t submit_epoch = 1; // bumped per queue submit: draw bindings check a texture's storage once per submit
// Mirrored textures the GPU wrote into, sorted by address (vk_texture_cpu_stale, vk_texture_write_back). Changed under
// tex_lock and owned_lock; read under either.
static PlatMutex owned_lock = PLAT_MUTEX_INIT;
static Tex **owned;
static int nowned, cowned;
static uint64_t max_owned; // largest storage size in owned[]
static uint64_t gpu_writes; // GPU writes into mirrored textures so far: Tex.gpu_seq
// Bumped whenever the entry a slot resolves to can change: an entry created or destroyed, released or taken back into
// use (the slot lookup and the follow of a finalized slot read only by_key, by_addr and the released flags).
static uint32_t tex_gen = 1;
// Bumped with tex_gen and whenever a slot is registered: what vk_texture_by_id returns for any slot may have changed.
static uint32_t slots_gen = 1;
static uint32_t samp_gen = 1; // bumped whenever a sampler slot is registered: vk_sampler_by_id may answer otherwise
static uint64_t n_checks; // draw-binding storage checks (make_fresh_snap) since the last perf line
// tex_gen per storage address (hashed): a slot resolves through the entries at its own address only (slot_resolve), so
// a slot's resolution stays valid while its address's generation does. TotK finalizes and initializes transient
// textures every frame: about 90 tex_gen and 160 slots_gen bumps per frame in the monsters fight, one every 16 draws,
// which emptied every cache keyed on the global generations.
#define AGEN_N 4096
static uint32_t agen[AGEN_N];
static uint32_t arev[AGEN_N]; // per storage address (hashed): the submit_epoch of the state scan's last revival there
// Per slot id, for vk_texture_slot_token without the lock: the registration serial (bits 16-47; 0: never registered)
// and the address generation index (bits 0-15), one word so a reader gets a consistent pair. Fixed-size (slot ids are
// 20 bits), unlike tslots, which moves when it grows.
static uint64_t *sinfo;
static uint32_t *sepoch; // per slot: the submit_epoch its entry was checked in (vk_texture_slot_checked), 0 none
static uint32_t reg_serial;
static uint32_t agen_idx(uint64_t addr) { return (uint32_t)(addr * 0x9E3779B97F4A7C15ull >> 52); }
static void tex_gen_bump(uint64_t addr) {
if (!++tex_gen) tex_gen = 1;
slots_gen++;
uint32_t *g = &agen[agen_idx(addr)];
__atomic_store_n(g, *g + 1, __ATOMIC_RELEASE);
}
static volatile int32_t tex_ready;
static void tex_init(void *p) {
(void)p;
__atomic_store_n(&tex_ready, 1, __ATOMIC_RELEASE);
return;
}
static _Thread_local int tex_depth; // how often this thread holds tex_lock (entry_of releases it for a wait)
static _Thread_local uint32_t tex_settles; // entry_of waits of this thread (vk_texture_by_id_locked)
static void lock(void) {
if (!__atomic_load_n(&tex_ready, __ATOMIC_ACQUIRE)) plat_once(&tex_once, tex_init, NULL);
plat_rmutex_lock(&tex_lock);
tex_depth++;
}
static void unlock(void) {
tex_depth--;
plat_rmutex_unlock(&tex_lock);
}
static uint32_t umax(uint32_t a, uint32_t b) { return a > b ? a : b; }
static uint32_t mip(uint32_t v, unsigned lv) { return umax(v >> lv, 1); }
static void make_key(const tex_t *t, TexKey *k) {
memset(k, 0, sizeof *k);
k->addr = (uintptr_t)t->mem;
k->d = t->d;
k->has_layout = t->has_layout;
memcpy(k->layout, t->layout, 8);
}
// Keys equal apart from the descriptor's swizzle selectors: the same storage, format and geometry.
// The colour format without its sRGB decode: a target and its sRGB view share one texel layout (slot_resolve).
static int linear_format(int f) {
switch (f) {
case NVN_FORMAT_RGBA8_SRGB: return NVN_FORMAT_RGBA8;
case NVN_FORMAT_RGBX8_SRGB: return NVN_FORMAT_RGBX8;
case NVN_FORMAT_BGRA8_SRGB: return NVN_FORMAT_BGRA8;
case NVN_FORMAT_BGRX8_SRGB: return NVN_FORMAT_BGRX8;
default: return f;
}
}
static int same_but_swizzle(const TexKey *a, const TexKey *b) {
TexKey x = *a, y = *b;
x.d.swizzle_r = x.d.swizzle_g = x.d.swizzle_b = x.d.swizzle_a = 0;
y.d.swizzle_r = y.d.swizzle_g = y.d.swizzle_b = y.d.swizzle_a = 0;
return !memcmp(&x, &y, sizeof x);
}
// MNG nvn_mtl.m tex_mirrored: textures whose content the CPU provides in pool memory. Display, compressible, zcull,
// depth, multisample and GPU-only storage stays GPU-owned.
static int tex_mirrored(const tex_t *t) {
if (!t->mem || nvn_gpu_only((uintptr_t)t->mem)) return 0;
if (t->has_layout) return 1;
if (!(t->pool_flags & (NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED | NVN_MEMORY_POOL_FLAGS_CPU_CACHED))) return 0;
if (t->d.flags & (NVN_TEXTURE_FLAGS_DISPLAY | NVN_TEXTURE_FLAGS_COMPRESSIBLE | NVN_TEXTURE_FLAGS_ADAPTIVE_ZCULL |
NVN_TEXTURE_FLAGS_DEPTH16_PREFER_FAST_CLEAR | NVN_TEXTURE_FLAGS_ZCULL_SUPPORT_STENCIL))
return 0;
if (t->d.samples > 1 || t->d.target == NVN_TEXTURE_TARGET_BUFFER) return 0;
if (t->d.format >= 0x31 && t->d.format <= 0x36) return 0; // stencil / depth formats
return 1;
}
static void log_limited(volatile int32_t *n, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static void log_limited(volatile int32_t *n, const char *fmt, ...) {
if (plat_inc32(n) > 16) return;
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
rt_log("vk: %s", buf);
}
static void barrier(VkCommandBuffer cb, VkPipelineStageFlags2 src, VkAccessFlags2 sa, VkPipelineStageFlags2 dst,
VkAccessFlags2 da) {
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = src, .srcAccessMask = sa,
.dstStageMask = dst, .dstAccessMask = da};
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
vkCmdPipelineBarrier2(cb, &di);
}
#define ALL_RW VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT
// ---------------------------------------------------------------- images
static VkFormatFeatureFlags features_of(VkFormat f) {
VkFormatProperties p;
vkGetPhysicalDeviceFormatProperties(vk_ctx()->phys, f, &p);
return p.optimalTilingFeatures;
}
static VkImageAspectFlags aspect_of(const TexVkFormat *f) {
VkImageAspectFlags a = 0;
if (f->depth) a |= VK_IMAGE_ASPECT_DEPTH_BIT;
if (f->stencil) a |= VK_IMAGE_ASPECT_STENCIL_BIT;
return a ? a : VK_IMAGE_ASPECT_COLOR_BIT;
}
// Binds img into the pool memory behind its GPU address (physical pools, virtual pools through the page table) when
// the memory type, alignment and contiguous size allow; 0 otherwise.
// A depth / stencil image that needs more bytes than its NVN storage (guest_size) is not bound: AMD's RDNA2 driver
// (26.10.1) puts the depth compression metadata after the depth data, and above 1080p that tail lies over the next
// texture of the game's heap; each depth write then scrambled it (garbage band at the top, pink glow, blocks in the
// grass, a blue checkered scene in the menu). Colour images keep their place: the RX 9070 also sizes some of them
// above their storage, and nothing there showed that the GPU writes the excess.
static int format_compressed(VkFormat f) { // vkCmdClearColorImage takes no block-compressed format
return (f >= VK_FORMAT_BC1_RGB_UNORM_BLOCK && f <= VK_FORMAT_ASTC_12x12_SRGB_BLOCK) ||
(f >= VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK && f <= VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK);
}
static int bind_pool_memory(VkCtx *c, Tex *e, VkImage img, uint64_t guest_size) {
VkMemoryRequirements req;
vkGetImageMemoryRequirements(c->device, img, &req);
if ((e->f.depth || e->f.stencil) && req.size > guest_size) return 0;
VkMemSpan sp;
if (!req.size) return 0; // an image the device could not lay out (image_size)
if (!vk_mem_resolve(e->pub.addr, req.size, &sp) || !sp.alloc || sp.alloc_offset % req.alignment) return 0;
VmaAllocationInfo info;
vmaGetAllocationInfo(c->vma, sp.alloc, &info);
if (!(req.memoryTypeBits >> info.memoryType & 1)) return 0;
if (vmaBindImageMemory2(c->vma, sp.alloc, sp.alloc_offset, img, NULL) != VK_SUCCESS) return 0;
e->bound_to = sp.alloc;
return 1;
}
// agl's BC4 encoder draws a BC texture's blocks through an RGBA16UI view, one texel per 8-byte block (refs/nxnative
// docs/agents/04-graphics.md, Breath of the Wild and Tears of the Kingdom sections). Vulkan does it with an uncompressed
// view of a BLOCK_TEXEL_VIEW_COMPATIBLE image; EXTENDED_USAGE lets the BC image carry the COLOR_ATTACHMENT usage that
// only such views support. Whether the device takes that combination for this format and shape:
static int block_views_ok(const VkImageCreateInfo *ci) {
VkPhysicalDeviceImageFormatInfo2 fi = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2,
.format = ci->format, .type = ci->imageType, .tiling = ci->tiling,
.usage = ci->usage, .flags = ci->flags};
VkImageFormatProperties2 p = {.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2};
if (vkGetPhysicalDeviceImageFormatProperties2(vk_ctx()->phys, &fi, &p) != VK_SUCCESS) return 0;
const VkImageFormatProperties *q = &p.imageFormatProperties;
return ci->extent.width <= q->maxExtent.width && ci->extent.height <= q->maxExtent.height &&
ci->extent.depth <= q->maxExtent.depth && ci->mipLevels <= q->maxMipLevels &&
ci->arrayLayers <= q->maxArrayLayers;
}
// The memory the device wants for an image made from ci: 0 when it cannot lay it out. RADV on GFX9 (Vega, Renoir)
// hands back a 0-byte image for a mipmapped block-compressed 3D image at least as deep as it is wide in blocks, with
// COLOR_ATTACHMENT usage and no STORAGE: that usage asks for a thin 3D layout (Mesa radv_image.c), for which the GFX9
// address library has no swizzle mode left (gfx9addrlib.cpp), and the error is dropped; VMA then refuses the 0-byte
// allocation. The format properties call it supported, so only the size shows it.
static VkDeviceSize image_size(const VkImageCreateInfo *ci) {
VkDeviceImageMemoryRequirements q = {.sType = VK_STRUCTURE_TYPE_DEVICE_IMAGE_MEMORY_REQUIREMENTS, .pCreateInfo = ci};
VkMemoryRequirements2 r = {.sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2};
vkGetDeviceImageMemoryRequirements(vk_ctx()->device, &q, &r);
return r.memoryRequirements.size;
}
// UNDEFINED -> GENERAL; new depth / stencil images start cleared (LAS textures.c: the game copies depth
// without stencil, and the stencil of those images must not be whatever the memory held).
static void init_image(Tex *e, VkCommandBuffer cb) {
VkImageMemoryBarrier2 ib = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_NONE,
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = e->pub.image, .subresourceRange = {e->pub.aspect, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS}};
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &ib};
vkCmdPipelineBarrier2(cb, &di);
if (e->f.depth || e->f.stencil) {
VkClearDepthStencilValue zero = {0.0f, 0};
VkImageSubresourceRange rg = {e->pub.aspect, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS};
vkCmdClearDepthStencilImage(cb, e->pub.image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &rg);
barrier(cb, VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
} else if (vk_fix_zero() && !e->bound_to && !e->pub.mirrored && !format_compressed(e->f.vk)) {
// GPU-only storage that got memory of its own starts at zero, as the pool it stands for (vk_memory.c
// alloc_physical). Images aliasing pool memory are not cleared: that would erase what other textures rendered
// there; mirrored images are uploaded from their storage on first use.
VkClearColorValue zero = {0};
VkImageSubresourceRange rg = {e->pub.aspect, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS};
vkCmdClearColorImage(cb, e->pub.image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &rg);
barrier(cb, VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
}
}
// TOTK_SHADOW_SCALE (1 to 4): the game's shadow maps are made this many times larger on each side. TotK renders its
// sun cascades into a 960x960 DEPTH16 2D array of 4 layers and its static shadows into 1440x1440 and 720x720 arrays of 2,
// whatever the output resolution, and shrinks the cascades under load (872, 848, 800, 480 and so on). Those are made
// larger here: render passes into them scale the game's viewports and scissors (vk_exec.c resolve_rt, pass_scale),
// copies scale their regions (vk_exec_xfer.c sub_resolve), and shaders sample them with normalised coordinates, so the
// game sees nothing of it. A map whose storage the CPU writes (mirrored) or that aliases pool memory keeps its size,
// as its texels must match the guest layout.
static int shadow_scale(void) {
static int scale = -1;
if (scale < 0) {
int s = cfg_int("TOTK_SHADOW_SCALE");
s = s < 1 ? 1 : s > 4 ? 4 : s;
rt_log("vk: shadow map scale %d", s);
scale = s;
}
return scale;
}
static int shadow_scale_of(const nvn_texdesc *d, const TexVkFormat *f, uint32_t samples, int mirrored) {
int scale = shadow_scale();
if (scale == 1 || mirrored || samples > 1 || d->format != NVN_FORMAT_DEPTH16 || !f->depth) return 1;
if (d->target != NVN_TEXTURE_TARGET_2D_ARRAY || d->width != d->height || d->width < 256 || d->depth < 2 || d->depth > 4)
return 1;
uint32_t most = vk_ctx()->props.limits.maxImageDimension2D;
return d->width * (uint32_t)scale <= most ? scale : 1;
}
// defer_init: the caller records the image's first transition (init_image or alias_copy).
static Tex *create_tex(const tex_t *t, const TexKey *k, int defer_init) {
static volatile int32_t n_unmapped, n_unsupported, n_failed, n_unbound, n_thick;
VkCtx *c = vk_ctx();
const nvn_texdesc *d = &t->d;
if (!c || d->target == NVN_TEXTURE_TARGET_BUFFER) return NULL;
TexVkFormat f;
if (!tex_vk_format(d->format, &f)) {
log_limited(&n_unmapped, "texture format %#x has no Vulkan format: %s", d->format, f.reason);
return NULL;
}
if (!tex_vk_format_supported(&f)) {
log_limited(&n_unsupported, "texture format %#x: VkFormat %d lacks features %#x", d->format, (int)f.vk, (unsigned)f.need);
return NULL;
}
nvn_texlayout L;
nvn_tex_layout(d, &L);
uint32_t w = umax(d->width, 1), h = umax(d->height, 1), depth = 1, layers = 1;
VkImageType type = VK_IMAGE_TYPE_2D;
switch (d->target) {
case NVN_TEXTURE_TARGET_1D: type = VK_IMAGE_TYPE_1D; h = 1; break;
case NVN_TEXTURE_TARGET_1D_ARRAY: type = VK_IMAGE_TYPE_1D; h = 1; layers = L.layers; break;
case NVN_TEXTURE_TARGET_3D: type = VK_IMAGE_TYPE_3D; depth = umax(d->depth, 1); break;
case NVN_TEXTURE_TARGET_2D_ARRAY: case NVN_TEXTURE_TARGET_2D_MULTISAMPLE_ARRAY:
case NVN_TEXTURE_TARGET_CUBEMAP: case NVN_TEXTURE_TARGET_CUBEMAP_ARRAY: layers = L.layers; break;
default: break;
}
int scale = shadow_scale_of(d, &f, d->samples > 1 ? (uint32_t)d->samples : 1, tex_mirrored(t));
w *= (uint32_t)scale, h *= (uint32_t)scale;
uint32_t levels = umax(d->levels, 1), most = 1;
for (uint32_t s = umax(umax(w, h), depth); s > 1; s >>= 1) most++;
if (levels > most) levels = most;
uint32_t samples = d->samples > 1 ? (uint32_t)d->samples : 1;
if (samples > 1) levels = 1;
VkFormatFeatureFlags ff = features_of(f.vk);
VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
int ds = f.depth || f.stencil;
if (ds && (ff & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
if (!ds && (ff & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
if ((d->flags & NVN_TEXTURE_FLAGS_IMAGE) && (ff & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) && samples == 1)
usage |= VK_IMAGE_USAGE_STORAGE_BIT;
// contact shadows (vk_contact.c) darken the game's B10G11R11 scene colour in place from a compute shader
if (f.vk == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && (usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) && samples == 1 &&
(ff & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) && w >= 640 && vk_contact_strength())
usage |= VK_IMAGE_USAGE_STORAGE_BIT;
VkImageCreateFlags flags = ds ? 0 : VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; // format-reinterpreting views
// Cube compatible only for cube targets: the replay makes cube views of nothing else (vk_exec_draw.c get_view checks
// the image's own view type). The flag on every square array of 6+ layers changed how AMD's Windows driver lays such
// an image out, and terrain material arrays sampled through it came out in rainbow blocks that moved with the mip
// level (Savage Wind Recomp issue #59, BotW on an RX 580 and a 7800 XT).
if (type == VK_IMAGE_TYPE_2D && layers >= 6 && w == h && samples == 1 &&
(d->target == NVN_TEXTURE_TARGET_CUBEMAP || d->target == NVN_TEXTURE_TARGET_CUBEMAP_ARRAY))
flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
// NVN renders into a 3D texture slice by slice (a view's min layer is the depth plane, nvn_mtl.m), e.g. the
// grading LUT of the menu background blur. Vulkan needs a 2D view of the slice for that.
if (type == VK_IMAGE_TYPE_3D && (usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)) flags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .flags = flags, .imageType = type, .format = f.vk,
.extent = {w, h, depth}, .mipLevels = levels, .arrayLayers = layers,
.samples = (VkSampleCountFlagBits)samples, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = usage,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
int bw, bh, block_views = 0;
// TOTK_BLOCK_VIEWS_3D (default 0): a 3D BC image also gets block views. On RDNA2 cards (driver 26.10.1, issue #5)
// the 32x32x32 BC4 volume the game creates at the logo, made BLOCK_TEXEL_VIEW_COMPATIBLE and 2D_ARRAY_COMPATIBLE,
// took a GPU write just past the end of its memory: device lost at the first submit after it when the image ended
// its memory block. Draws into a 3D BC image go through a stand-in instead (vk_exec.c bc_target).
static int bv3d = -1;
if (bv3d < 0) {
bv3d = rt_knob("BLOCK_VIEWS_3D", "0");
rt_log("vk: 3D BC textures %s (TOTK_BLOCK_VIEWS_3D=%d)", bv3d ? "get block views (renderable)" : "have no block views",
bv3d);
}
if (!ds && samples == 1 && f.vk == f.ideal && nvn_fmt_bpb(d->format, &bw, &bh) && bw == 4 && bh == 4 &&
(type != VK_IMAGE_TYPE_3D || bv3d)) {
VkImageCreateInfo bc = ci; // BC formats: renderable through views of their blocks (block_views_ok)
bc.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
bc.flags |= VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT;
if (type == VK_IMAGE_TYPE_3D) bc.flags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
VkImageUsageFlags hidden = 0; // creation usage the replay never uses (views restrict theirs, vk_exec.c)
int ok = block_views_ok(&bc);
if (ok && type == VK_IMAGE_TYPE_3D && !image_size(&bc)) {
// STORAGE usage keeps RADV on GFX9 from the thin layout (image_size), so agl's BC4 encoder still renders
// into the volume; else the image is made without block views and its encoder passes are left out
bc.usage |= VK_IMAGE_USAGE_STORAGE_BIT, hidden = VK_IMAGE_USAGE_STORAGE_BIT;
ok = block_views_ok(&bc) && image_size(&bc);
log_limited(&n_thick, "texture fmt %#x %ux%ux%u: the device cannot lay out a 3D image with block views; %s",
d->format, w, h, depth, ok ? "made with storage usage (a thick layout)" : "made without them");
}
if (ok) ci = bc, usage = bc.usage & ~hidden, flags = bc.flags, block_views = 1;
}
// Feedback loops (vk.h VkCtx.feedback): every attachable image may be sampled by a draw of a pass that renders it,
// so it gets the usage now; adding it when a loop shows up would mean recreating the image (that lost the
// grass tile maps once).
int fb_usage = c->feedback == VK_FEEDBACK_LAYOUT && !block_views &&
(usage & (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT));
if (fb_usage) ci.usage = usage |= VK_IMAGE_USAGE_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT;
Tex *e = calloc(1, sizeof *e);
if (!e) rt_fatal("vk: out of memory for a texture");
e->key = *k;
e->t = *t;
e->f = f;
e->pub.addr = k->addr;
VkImage img = VK_NULL_HANDLE;
if (scale == 1 && nvn_gpu_only(k->addr)) { // storage in a physical or virtual pool: alias the pool memory (section 3.2)
if (vkCreateImage(c->device, &ci, NULL, &img) == VK_SUCCESS && !bind_pool_memory(c, e, img, L.size)) {
vkDestroyImage(c->device, img, NULL);
img = VK_NULL_HANDLE;
log_limited(&n_unbound, "texture %#llx (%ux%u fmt %#x) cannot alias its pool memory; own memory instead",
(unsigned long long)k->addr, w, h, d->format);
}
}
if (!img) {
VkResult r = rt_oom_forced(RT_OOM_IMAGE) ? VK_ERROR_OUT_OF_DEVICE_MEMORY
: vk_texture_image(&ci, &img, &e->alloc);
// Out of memory: one more try after the headroom blocks and the finished deferred objects are freed
// (vk_oom_release, which takes cmd_lock after the texture lock held here); released textures go at the next
// submit. No wait: the texture is asked for again at its next use, while a wait would hold the texture lock.
if (r == VK_ERROR_OUT_OF_HOST_MEMORY || r == VK_ERROR_OUT_OF_DEVICE_MEMORY) {
static volatile int32_t n_retried;
vk_oom_release();
VkResult again = vk_texture_image(&ci, &img, &e->alloc);
log_limited(&n_retried, "image creation failed (%d), tried again after a memory release: %s (%ux%ux%u fmt %#x)",
(int)r, again == VK_SUCCESS ? "created" : "failed again", w, h, depth, d->format);
r = again;
}
if (r != VK_SUCCESS) {
log_limited(&n_failed, "image creation failed (%d): nvn fmt %#x %ux%ux%u target %d levels %u layers %u", (int)r,
d->format, w, h, depth, d->target, levels, layers);
free(e);
return NULL;
}
}
VkTexImage *p = &e->pub;
p->image = img;
p->format = f.vk;
p->type = type;
p->usage = usage;
p->aspect = aspect_of(&f);
p->width = w, p->height = h, p->depth = depth, p->levels = levels, p->layers = layers, p->samples = samples;
p->view_type = type == VK_IMAGE_TYPE_1D ? (d->target == NVN_TEXTURE_TARGET_1D_ARRAY ? VK_IMAGE_VIEW_TYPE_1D_ARRAY : VK_IMAGE_VIEW_TYPE_1D)
: type == VK_IMAGE_TYPE_3D ? VK_IMAGE_VIEW_TYPE_3D
: d->target == NVN_TEXTURE_TARGET_CUBEMAP && (flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) ? VK_IMAGE_VIEW_TYPE_CUBE
: d->target == NVN_TEXTURE_TARGET_CUBEMAP_ARRAY && (flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) ? VK_IMAGE_VIEW_TYPE_CUBE_ARRAY
: layers > 1 || d->target == NVN_TEXTURE_TARGET_2D_ARRAY || d->target == NVN_TEXTURE_TARGET_2D_MULTISAMPLE_ARRAY
? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
p->alpha_one = f.alpha_one;
memcpy(p->chan, f.chan, sizeof p->chan);
p->swz[0] = (uint8_t)d->swizzle_r, p->swz[1] = (uint8_t)d->swizzle_g;
p->swz[2] = (uint8_t)d->swizzle_b, p->swz[3] = (uint8_t)d->swizzle_a;
if (!(p->swz[0] | p->swz[1] | p->swz[2] | p->swz[3])) // descriptors built without nvnTextureBuilderSetDefaults
p->swz[0] = NVN_TEXTURE_SWIZZLE_R, p->swz[1] = NVN_TEXTURE_SWIZZLE_G, p->swz[2] = NVN_TEXTURE_SWIZZLE_B,
p->swz[3] = NVN_TEXTURE_SWIZZLE_A;
p->mirrored = (uint8_t)tex_mirrored(t);
p->bound = e->bound_to != NULL;
p->block_views = (uint8_t)block_views;
p->fb_usage = (uint8_t)fb_usage;
p->scale = (uint8_t)scale;
if (!defer_init) {
init_image(e, vk_upload_begin());
vk_upload_end();
}
return e;
}
static int owned_lower(uint64_t a) {
int lo = 0, hi = nowned;
while (lo < hi) {
int m = (lo + hi) / 2;
if (owned[m]->pub.addr < a) lo = m + 1;
else hi = m;
}
return lo;
}
static void own_add(Tex *e) {
if (e->listed) return;
plat_mutex_lock(&owned_lock);
if (nowned == cowned) {
cowned = cowned ? 2 * cowned : 256;
if (!(owned = realloc(owned, sizeof *owned * (size_t)cowned))) rt_fatal("vk: out of memory for the owned-texture list");
}
int i = owned_lower(e->pub.addr);
memmove(owned + i + 1, owned + i, sizeof *owned * (size_t)(nowned - i));
owned[i] = e;
__atomic_store_n(&nowned, nowned + 1, __ATOMIC_RELEASE);
if (e->t.L.size > max_owned) max_owned = e->t.L.size;
e->listed = 1;
plat_mutex_unlock(&owned_lock);
}
static void own_remove(Tex *e) {
if (!e->listed) return;
plat_mutex_lock(&owned_lock);
for (int i = owned_lower(e->pub.addr); i < nowned && owned[i]->pub.addr == e->pub.addr; i++)
if (owned[i] == e) {
memmove(owned + i, owned + i + 1, sizeof *owned * (size_t)(nowned - i - 1));
__atomic_store_n(&nowned, nowned - 1, __ATOMIC_RELEASE);
break;
}
e->listed = 0;
plat_mutex_unlock(&owned_lock);
}
static uint64_t own_bytes(const Tex *e);
static void rel_append(Tex *e) {
e->rel_bytes = own_bytes(e);
rel_bytes_total += e->rel_bytes;
e->rel_next = NULL;
e->rel_prev = rel_tail;
if (rel_tail) rel_tail->rel_next = e;
else rel_head = e;
rel_tail = e;
}
static void rel_unlink(Tex *e) {
rel_bytes_total -= e->rel_bytes;
e->rel_bytes = 0;
if (e->rel_prev) e->rel_prev->rel_next = e->rel_next;
else rel_head = e->rel_next;
if (e->rel_next) e->rel_next->rel_prev = e->rel_prev;
else rel_tail = e->rel_prev;
e->rel_prev = e->rel_next = NULL;
}
// A clock read per submit, for stamps on paths that run many times a frame.
static uint64_t coarse_ms;
static uint64_t use_counter;
// How long released entries wait until the game takes them back, split into those holding GPU-written content and the
// others (TOTK_TRACE=perf, vk_texture_released_log): where transient textures end and the tail that can go begins.
enum { AGE_1S, AGE_5S, AGE_30S, AGE_120S, AGE_LONG, AGE_N };
static const char *const age_name[AGE_N] = {"<1s", "<5s", "<30s", "<120s", ">=120s"};
static uint64_t takeback_n[2][AGE_N];
static int age_bucket(uint64_t ms) {
return ms < 1000 ? AGE_1S : ms < 5000 ? AGE_5S : ms < 30000 ? AGE_30S : ms < 120000 ? AGE_120S : AGE_LONG;
}
// Keys of entries the pressure path dropped, to count the ones the game asks for again soon after: a drop that buys
// nothing and, for GPU-written content, loses it. Direct-mapped, a lost record only undercounts.
#define DROPPED_SLOTS 4096u
static struct { uint64_t hash, ms; } dropped[DROPPED_SLOTS];
static uint64_t n_recreated[2], n_recreated_written[2]; // index: within 10 s, later
static uint64_t key_hash(const TexKey *k) {
uint64_t h = 0xcbf29ce484222325ull;
const uint8_t *b = (const uint8_t *)k;
for (size_t i = 0; i < sizeof *k; i++) h = (h ^ b[i]) * 0x100000001b3ull;
return h | 1;
}
static void dropped_note(const Tex *e) {
uint64_t h = key_hash(&e->key);
dropped[h >> 3 & (DROPPED_SLOTS - 1)].hash = h ^ (uint64_t)(e->pub.gpu_owned != 0) << 1;
dropped[h >> 3 & (DROPPED_SLOTS - 1)].ms = coarse_ms;
}
static void dropped_check(const TexKey *k) {
uint64_t h = key_hash(k);
unsigned i = h >> 3 & (DROPPED_SLOTS - 1);
uint64_t was = dropped[i].hash;
if (!was || (was & ~2ull) != (h & ~2ull)) return;
int later = coarse_ms - dropped[i].ms >= 10000;
n_recreated[later]++;
if (was != h) n_recreated_written[later]++;
dropped[i].hash = 0;
}
static void destroy_tex(Tex *e) {
own_remove(e);
tex_gen_bump(e->key.addr);
nvn_hm_del(&by_key, &e->key);
Tex *head = nvn_hm_get(&by_addr, &e->key.addr);
if (head == e) {
if (e->next_at) nvn_hm_put(&by_addr, &e->key.addr, e->next_at);
else nvn_hm_del(&by_addr, &e->key.addr);
} else {
for (Tex *p = head; p; p = p->next_at)
if (p->next_at == e) { p->next_at = e->next_at; break; }
}
if (e->released) nreleased--, rel_unlink(e);
// Another thread may hold &e->pub, taken under the lock and used after it (vk_texture_get, vk_texture_by_id: render
// targets for a whole pass, vk_exec.c resolve_target, vk_texture_storage_bound_locked writes through it). Freed
// here, that thread read a stale VkImage from freed memory, made views of it and cached them by that handle. The
// record goes with the image, once the submission being recorded is done; gone keeps views of it out of the caches.
e->pub.gone = 1;
vk_exec_image_gone(e->pub.image);
vk_defer_image(e->pub.image, e->alloc);
free(e->snap);
e->snap = NULL;
vk_defer_free(e);
}
// Released entries with another descriptor at the same address are replaced; past RELEASED_MAX released entries the
// oldest one goes at each release (it comes back on demand, a mirrored one re-uploaded from its storage). TotK
// finalizes and initializes transient textures every frame, so a released entry is mostly taken back within frames.
// Dropping all of them at once when the count passed 4096 destroyed thousands of images in one frame: 333-422 ms
// frames every 40-50 s on a GTX 1050 Ti, then the textures still in use were created and uploaded again.
#define RELEASED_MAX 2048
static void sweep_released(void) {
while (nreleased > RELEASED_MAX && rel_head) destroy_tex(rel_head);
}
// TOTK_TEX_VRAM_PRESSURE (percent of the device-local budget, default 90; 0 = off): above it, each budget query drops up
// to PRESSURE_STEP released entries, oldest first, until the use is back under the line; a few at a time, for the reason
// above. Two kinds come back without loss:
//
// Overwritten: the game placed another texture on the entry's storage after releasing it, so on the Switch its bytes are
// gone too, and it has waited DEAD_AGE_MS. Transient render targets are taken back within a second or two (a stand-still
// field window: about 19000 take-backs under 1 s, 2 later than that, none after 30 s), while their leftovers at
// addresses the game's allocator moved on from stay released for good: 1.9 GiB of 2.0 GiB, nearly all of it render targets
// no storage can refill. Not bound through a finalized slot for DEAD_QUIET_SUBMITS either: such a slot still samples the
// entry (slot_resolve).
//
// Clean: mirrored ones the GPU never wrote, re-uploaded from their storage, at the cost of the upload, so after
// CLEAN_AGE_MS: without the wait a fight re-created about 500 of them per 10 s. Any other render target lives only in its
// image: dropping one that was merely old brought the texture back empty (black flashes in a Depths boss fight, ~170
// "left uninitialised" aliases a minute).
//
// TOTK_TEX_RELEASED_MIB (default 256; 0 = no cap): the same drops run while the released entries hold more own memory than
// that, under pressure or not. A 10-minute field run on the RX 9070 kept 818 MiB of them, 730 MiB with their storage
// taken by a later texture, while 23579 of 23660 take-backs came within a second and none after 120 s. With the
// cap the same route ended with 216 MiB of them, 1.1 GB less process commit and 1 GB less in VMA blocks; 15 dropped
// textures were created again, and the slow frames stayed the same.
#define PRESSURE_STEP 32
#define CLEAN_AGE_MS 5000u
#define DEAD_AGE_MS 30000u
#define DEAD_QUIET_SUBMITS 600u
#define DEAD_RETRY_QUERIES 8 // budget queries to skip after a search that found nothing
static int vram_pressure = -1;
static uint64_t released_cap;
static uint64_t n_pressure_drops, n_pressure_overwritten;
static int tex_by_addr(const void *a, const void *b) {
const Tex *x = *(Tex *const *)a, *y = *(Tex *const *)b;
return x->pub.addr < y->pub.addr ? -1 : x->pub.addr > y->pub.addr;
}
// All entries sorted by storage address, with the highest storage end up to each one: the input of storage_taken.
typedef struct { Tex **e; uint64_t *end; int n; } AddrIndex;
static int addr_index(AddrIndex *x) {
x->n = 0;
x->e = malloc(sizeof *x->e * (size_t)(by_key.n + 1));
x->end = malloc(sizeof *x->end * (size_t)(by_key.n + 1));
if (!x->e || !x->end) { free(x->e), free(x->end); return 0; }
for (uint32_t i = 0; i < by_key.cap; i++)
if (by_key.vals[i]) x->e[x->n++] = by_key.vals[i];
qsort(x->e, (size_t)x->n, sizeof *x->e, tex_by_addr);
uint64_t top = 0;
for (int i = 0; i < x->n; i++) {
uint64_t end = x->e[i]->pub.addr + x->e[i]->t.L.size;
x->end[i] = top = end > top ? end : top;
}
return 1;
}
static void addr_index_free(AddrIndex *x) { free(x->e), free(x->end); }
// A texture placed after the release of x->e[i] overlaps its storage.
static int storage_taken(const AddrIndex *x, int i) {
const Tex *e = x->e[i];
for (int j = i; --j >= 0 && x->end[j] > e->pub.addr;)
if (x->e[j]->pub.addr + x->e[j]->t.L.size > e->pub.addr && x->e[j]->use_seq > e->rel_seq) return 1;
for (int j = i; ++j < x->n && x->e[j]->pub.addr < e->pub.addr + e->t.L.size;)
if (x->e[j]->use_seq > e->rel_seq) return 1;
return 0;
}
static uint64_t own_bytes(const Tex *e) {
VkCtx *c = vk_ctx();
if (!e->alloc || !c) return 0;
VmaAllocationInfo ai;
vmaGetAllocationInfo(c->vma, e->alloc, &ai);
return ai.size;
}
static int dead_candidate(const Tex *e) {
return e->alloc && (!e->checked || submit_epoch - e->checked >= DEAD_QUIET_SUBMITS);
}
// Drops overwritten entries until `need` bytes are free or the step is used up. The sort runs only under pressure, while
// an old enough entry exists, and rests for a while when it finds nothing to drop. Returns the bytes freed.
static uint64_t pressure_search_ns; // longest search since the last mem: released line
// step: PRESSURE_STEP, or OOM_STEP from oom_release, which neither waits out nor starts the rest.
#define OOM_STEP 256
static uint64_t drop_overwritten(uint64_t need, int *dropped, int step) {
static int rest;
int urgent = step > PRESSURE_STEP;
if (rest > 0 && !urgent) { rest--; return 0; }
if (!rel_head || coarse_ms - rel_head->rel_ms < DEAD_AGE_MS) return 0;
uint64_t t0 = plat_now_ns(), freed = 0;
AddrIndex x;
if (!addr_index(&x)) return 0;
Tex *cand[OOM_STEP];
int nc = 0;
for (Tex *e = rel_head; e && nc < step && coarse_ms - e->rel_ms >= DEAD_AGE_MS; e = e->rel_next) {
if (!dead_candidate(e)) continue;
int lo = 0, hi = x.n; // the first entry at e's address, then e among those
while (lo < hi) {
int m = (lo + hi) / 2;
if (x.e[m]->pub.addr < e->pub.addr) lo = m + 1;
else hi = m;
}
while (lo < x.n && x.e[lo] != e) lo++;
if (lo < x.n && storage_taken(&x, lo)) cand[nc++] = e;
}
addr_index_free(&x);
for (int i = 0; i < nc && freed < need; i++) {
freed += own_bytes(cand[i]);
dropped_note(cand[i]);
destroy_tex(cand[i]);
(*dropped)++, n_pressure_drops++, n_pressure_overwritten++;
}
if (!nc && !urgent) rest = DEAD_RETRY_QUERIES;
uint64_t ns = plat_now_ns() - t0;
if (ns > pressure_search_ns) pressure_search_ns = ns;
return freed;
}
// Clean released entries, oldest first: mirrored ones the GPU never wrote, which come back from their storage.
static uint64_t drop_clean(uint64_t need, uint64_t freed, int *dropped, int step) {
for (Tex *e = rel_head, *next; e && freed < need && *dropped < step && coarse_ms - e->rel_ms >= CLEAN_AGE_MS; e = next) {
next = e->rel_next;
if (!e->pub.mirrored || e->pub.gpu_owned || e->gpu_dirty) continue;
freed += own_bytes(e);
dropped_note(e);
destroy_tex(e);
(*dropped)++, n_pressure_drops++;
}
return freed;
}
static void sweep_pressure(void) { // tex_lock held
if (vram_pressure < 0) {
const char *s = cfg_get("TOTK_TEX_VRAM_PRESSURE");
vram_pressure = s && *s ? atoi(s) : 90;
if (vram_pressure < 0 || vram_pressure > 100) vram_pressure = 90;
const char *m = cfg_get("TOTK_TEX_RELEASED_MIB");
released_cap = (m && *m ? strtoull(m, NULL, 10) : 256) << 20;
rt_log("vk: released textures under VRAM pressure: %s%d%%; above %llu MiB of them: %s (TOTK_TEX_RELEASED_MIB)",
vram_pressure ? "dropped above " : "off", vram_pressure, (unsigned long long)(released_cap >> 20),
released_cap ? "dropped" : "no cap");
}
if (!rel_head || submit_epoch & 7) return; // the budget query every 8 submits
uint64_t need = released_cap && rel_bytes_total > released_cap ? rel_bytes_total - released_cap : 0;
uint64_t used, budget;
int exact;
if (vram_pressure && vk_mem_vram(&used, &budget, &exact)) {
uint64_t line = budget / 100 * (uint64_t)vram_pressure;
if (used > line && used - line > need) need = used - line;
}
if (!need) return;
int dropped = 0;
uint64_t freed = drop_overwritten(need, &dropped, PRESSURE_STEP);
drop_clean(need, freed, &dropped, PRESSURE_STEP);
}
// rt_oom_request (an allocation failed somewhere, oom.c): the same two kinds as sweep_pressure, as many as OOM_STEP
// whatever the budget says, and the headroom blocks. Never GPU-written content that lives only in its image. The memory
// comes back once the submission being recorded is done (destroy_tex defers the images). Texture lock held.
static void oom_release(void) {
int dropped = 0;
uint64_t freed = drop_overwritten(UINT64_MAX, &dropped, OOM_STEP);
int overwritten = dropped;
freed = drop_clean(UINT64_MAX, freed, &dropped, OOM_STEP);
vk_headroom_release();
static volatile int32_t n_logged;
log_limited(&n_logged, "out of memory: %d released textures dropped (%d overwritten, %d clean), %.0f MiB, %d kept",
dropped, overwritten, dropped - overwritten, freed / 1048576.0, nreleased);
}
// ---------------------------------------------------------------- uploads
static uint32_t texel_alpha_one(int format, uint32_t *elem) {
switch (format) {
case 0x19: *elem = 1; return 0xFF;
case 0x1A: *elem = 1; return 0x7F;
case 0x1B: case 0x1C: *elem = 1; return 1;
case 0x1D: *elem = 2; return 0x3C00;
case 0x1E: *elem = 2; return 0xFFFF;
case 0x1F: *elem = 2; return 0x7FFF;
case 0x20: case 0x21: *elem = 2; return 1;
case 0x22: *elem = 4; return 0x3F800000;
default: *elem = 4; return 1; // 0x23, 0x24
}
}
// 3-component texels to 4 (n texels).
static void expand_rgb(int format, const uint8_t *src, uint8_t *dst, size_t n) {
uint32_t e, one = texel_alpha_one(format, &e);
for (size_t i = 0; i < n; i++, src += 3 * e, dst += 4 * e) {
memcpy(dst, src, 3 * e);
memcpy(dst + 3 * e, &one, e);
}
}
static void shrink_rgb(int format, const uint8_t *src, uint8_t *dst, size_t n) {
uint32_t e;
texel_alpha_one(format, &e);
for (size_t i = 0; i < n; i++, src += 4 * e, dst += 3 * e) memcpy(dst, src, 3 * e);
}
// TOTK_ASTC=check: the compute decoder's texels of n regions (gpu: the regions one after the other) against
// astc_decode_ldr of the same raw blocks (raw: the staging bytes the regions point into).
static void astc_check(const Tex *e, const uint8_t *raw, const VkAstcRegion *ar, int n, const uint8_t *gpu) {
static volatile int32_t n_bad;
static uint64_t n_tex, n_texels, n_diff_tex;
uint32_t bw = e->f.astc_bw, bh = e->f.astc_bh;
uint64_t diff = 0, first = UINT64_MAX;
uint32_t want = 0, got = 0;
int first_k = 0;
for (int k = 0; k < n; k++) {
const VkAstcRegion *r = &ar[k];
size_t sb = (size_t)((r->w + bw - 1) / bw) * ((r->h + bh - 1) / bh) * 16, st = (size_t)r->w * r->h * 4;
uint8_t *cpu = malloc(st);
for (uint32_t z = 0; z < r->slices; z++, gpu += st) {
astc_decode_ldr(raw + r->src + z * sb, r->w, r->h, bw, bh, e->f.astc_srgb, cpu);
if (!memcmp(cpu, gpu, st)) continue;
for (size_t i = 0; i < st / 4; i++)
if (memcmp(cpu + 4 * i, gpu + 4 * i, 4)) {
if (first == UINT64_MAX) {
first = i, first_k = k;
memcpy(&want, cpu + 4 * i, 4), memcpy(&got, gpu + 4 * i, 4);
}
diff++;
}
}
free(cpu);
n_texels += (uint64_t)r->w * r->h * r->slices;
}
n_tex++;
if (diff) {
n_diff_tex++;
log_limited(&n_bad, "ASTC check: texture %#llx fmt %#x %ux%u: %llu texels differ, first region %d texel %llu "
"cpu %08x gpu %08x", (unsigned long long)e->pub.addr, e->t.d.format, bw, bh,
(unsigned long long)diff, first_k, (unsigned long long)first, want, got);
}
if (n_tex == 1 || !(n_tex & 63) || diff)
rt_log("vk: ASTC check: %llu textures, %llu texels compared, %llu textures differ", (unsigned long long)n_tex,
(unsigned long long)n_texels, (unsigned long long)n_diff_tex);
}
// Staging bytes of one part of a texture upload decoded or copied on the CPU (see upload). With TOTK_ASTC=cpu a
// large ASTC array decodes to RGBA8 in one go: a staging buffer of 676682336 bytes failed (-2) on an Intel UHD with
// 8 GB of shared memory and the game closed before the title screen.
#define UPLOAD_PART (32ull << 20) // fits the staging pool (vk_staging takes up to half of its 64 MiB block)
// The open upload batch is submitted between chunks (and parts of the CPU path) once its staging and scratch pass this:
// on fight2's first world frame one batch took hundreds of MiB, and the staging and scratch pools grew by 64 MiB blocks
// on the replay thread. Submitting earlier keeps the queue order (the batch still precedes the part that samples the
// images) and lets the GPU finish and return the buffers sooner.
#define UPLOAD_HELD_MAX (48ull << 20)
static int upload_gpu(Tex *e);
// The CPU side of upload: deswizzles (and decodes or expands) into staging buffers, then copies them into the image.
static void upload_cpu(Tex *e) {
static volatile int32_t n_bad, n_undecoded, n_split;
const tex_t *t = &e->t;
const nvn_texdesc *d = &t->d;
int bw, bh, bpb = nvn_fmt_bpb(d->format, &bw, &bh);
int linear = nvn_tex_is_linear(d), is3d = d->target == NVN_TEXTURE_TARGET_3D, one_d = e->pub.type == VK_IMAGE_TYPE_1D;
uint32_t levels = linear ? 1 : e->pub.levels, layers = linear ? 1 : e->pub.layers;
int astc = e->f.astc_bw != 0, rgb = e->f.expand_rgb;
int agpu = astc && vk_astc_gpu(), acheck = agpu && vk_astc_mode() == VK_ASTC_CHECK; // raw blocks to the GPU
uint32_t n = levels * layers, k = 0;
VkBufferImageCopy *rg = calloc(n, sizeof *rg);
VkAstcRegion *ar = agpu ? calloc(n, sizeof *ar) : NULL;
VkDeviceSize total = 0, decoded = 0;
for (uint32_t lv = 0; lv < levels; lv++)
for (uint32_t ly = 0; ly < layers; ly++) {
uint32_t w = mip(d->width, lv), h = one_d ? 1 : mip(d->height, lv), sl = is3d ? mip(d->depth, lv) : 1;
uint64_t texels = (uint64_t)w * h * sl, raw = (uint64_t)(w + bw - 1) / bw * bpb * ((h + bh - 1) / bh) * sl;
uint64_t out = agpu ? raw : astc ? texels * 4 : rgb ? texels * (uint64_t)bpb / 3 * 4 : raw;
rg[k++] = (VkBufferImageCopy){.bufferOffset = total, .imageSubresource = {e->pub.aspect, lv, ly, 1},
.imageExtent = {w, h, sl}};
total += (out + 15) & ~15ull;
decoded += texels * 4;
}
VkBuffer sb, rbuf = VK_NULL_HANDLE;
VmaAllocation ralloc = NULL;
uint8_t *rmap = NULL, *raw_copy = NULL;
int checked = 0, parts = 1;
// A CPU decode or copy stages at most UPLOAD_PART bytes per staging buffer (a region larger than that alone) and
// records one copy per buffer; every part stays in the same upload batch, submitted as before. The GPU decoder
// takes the raw blocks of every region from one buffer.
for (uint32_t k0 = 0, k1; k0 < n; k0 = k1) {
VkDeviceSize base = rg[k0].bufferOffset, size;
for (k1 = k0 + 1; k1 < n && (agpu || (k1 + 1 < n ? rg[k1 + 1].bufferOffset : total) - base <= UPLOAD_PART);) k1++;
size = (k1 < n ? rg[k1].bufferOffset : total) - base;
if (k0) parts++;
VkCommandBuffer cb = vk_upload_begin();
uint8_t *stg = vk_staging(size, &sb, NULL);
if (acheck) rmap = vk_staging(decoded, &rbuf, &ralloc);
for (k = k0; k < k1; k++) {
uint32_t lv = rg[k].imageSubresource.mipLevel, ly = rg[k].imageSubresource.baseArrayLayer;
uint32_t w = rg[k].imageExtent.width, h = rg[k].imageExtent.height, sl = rg[k].imageExtent.depth;
uint32_t rb = (w + bw - 1) / bw * (uint32_t)bpb, rows = (h + bh - 1) / bh;
size_t raw = (size_t)rb * rows * sl;
rg[k].bufferOffset -= base;
uint8_t *dst = stg + rg[k].bufferOffset, *tmp = (astc && !agpu) || rgb ? malloc(raw) : NULL;
if (nvn_deswizzle(d, lv, ly, t->mem, tmp ? tmp : dst, rb)) {
log_limited(&n_bad, "texture %#llx fmt %#x target %d: level %u layer %u has no CPU layout; uploaded as zeros",
(unsigned long long)e->pub.addr, d->format, d->target, lv, ly);
memset(tmp ? tmp : dst, 0, raw);
}
if (agpu) {
ar[k] = (VkAstcRegion){.src = rg[k].bufferOffset, .w = w, .h = h, .slices = sl, .level = lv, .layer = ly};
} else if (astc) {
uint64_t a0 = perf_begin();
for (uint32_t z = 0; z < sl; z++)
astc_decode_ldr_mt(tmp + (size_t)z * rb * rows, w, h, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb,
dst + (size_t)z * w * h * 4);
perf_end(PERF_ASTC, a0);
rg[k].bufferRowLength = w, rg[k].bufferImageHeight = h;
} else if (rgb) {
expand_rgb(d->format, tmp, dst, (size_t)w * h * sl);
rg[k].bufferRowLength = w, rg[k].bufferImageHeight = h;
} else {
rg[k].bufferRowLength = rb / (uint32_t)bpb * (uint32_t)bw, rg[k].bufferImageHeight = rows * (uint32_t)bh;
}
free(tmp);
}
if (acheck) {
raw_copy = malloc(size);
memcpy(raw_copy, stg, size);
}
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
if (agpu) {
uint64_t a0 = perf_begin();
checked = vk_astc_record(cb, sb, size, ar, (int)n, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb, e->f.astc_bc,
e->pub.image, e->pub.aspect, rbuf);
perf_end(PERF_ASTC, a0);
if (!checked)
log_limited(&n_undecoded, "ASTC texture %#llx (fmt %#x %ux%u) not decoded on the GPU: left as it was",
(unsigned long long)e->pub.addr, d->format, d->width, d->height);
} else {
vkCmdCopyBufferToImage(cb, sb, e->pub.image, VK_IMAGE_LAYOUT_GENERAL, k1 - k0, rg + k0);
}
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
vk_upload_end();
if (!acheck && vk_upload_held() > UPLOAD_HELD_MAX) vk_upload_flush(0);
}
if (parts > 1)
log_limited(&n_split, "texture %#llx fmt %#x %ux%u, %u levels, %u layers: %.1f MiB staged in %d parts",
(unsigned long long)e->pub.addr, d->format, d->width, d->height, levels, layers, total / 1048576.0,
parts);
if (acheck) { // waits for the GPU
vk_upload_flush(1);
if (checked) {
vmaInvalidateAllocation(vk_ctx()->vma, ralloc, 0, VK_WHOLE_SIZE);
astc_check(e, raw_copy, ar, (int)n, rmap);
}
vmaDestroyBuffer(vk_ctx()->vma, rbuf, ralloc);
}
free(raw_copy);
free(ar);
free(rg);
}
// Block-linear storage in an imported pool, deswizzled by the GPU (vk_deswizzle.c): the replay thread only records
// the dispatches, the image copies and (ASTC) the decode into the upload batch the CPU path also uses, which runs before
// the part that samples the image. The GPU reads the pool when the batch runs, as the Switch's GPU reads it at
// execution; a CPU write after make_fresh's write-watch reset still marks the pages and brings another upload. Each
// chunk of regions gets a device-local scratch buffer of at most DESWZ_CHUNK bytes (one region, when larger), freed
// once the batch completes. TOTK_DESWIZZLE=check compares every chunk with the CPU deswizzle (waits for the GPU).
// 0 when not handled, before anything is recorded: the CPU path takes the texture.
#define DESWZ_CHUNK (32ull << 20)
// Uploads by path since the last perf summary (TOTK_TRACE=perf): 0 GPU, else why the CPU took it.
enum { UP_GPU, UP_MODE, UP_LINEAR, UP_RGB, UP_ASTC_CPU, UP_BPB, UP_NOT_LIVE, UP_GEOM, UP_LIMITS, UP_SCRATCH, UP_WHY };
static const char *const up_why_name[UP_WHY] = {"gpu", "cpu mode", "linear", "rgb", "astc on cpu", "bytes per block",
"not an imported pool", "layout", "device limits", "scratch"};
static uint64_t up_n[UP_WHY], up_bytes[UP_WHY], up_ns[UP_WHY];
static int up_why; // upload_gpu's reason for returning 0 (texture lock held)
static int upload_gpu(Tex *e) {
static volatile int32_t n_scratch_fail, n_undecoded, n_bad;
static volatile int64_t n_checked, n_differ;
const tex_t *t = &e->t;
const nvn_texdesc *d = &t->d;
int mode = vk_deswz_mode();
up_why = mode == VK_DESWZ_CPU ? UP_MODE : nvn_tex_is_linear(d) ? UP_LINEAR : e->f.expand_rgb ? UP_RGB : UP_GPU;
if (up_why) return 0;
int astc = e->f.astc_bw != 0, agpu = astc && vk_astc_gpu();
if (astc && (!agpu || vk_astc_mode() == VK_ASTC_CHECK)) return up_why = UP_ASTC_CPU, 0; // or ASTC's check mode
int bw, bh, bpb = nvn_fmt_bpb(d->format, &bw, &bh);
if (!bpb || 16 % bpb) return up_why = UP_BPB, 0; // the rows of the linear copy are padded to 16 bytes
VkMemSpan sp;
if (!vk_mem_resolve(e->pub.addr, t->L.size, &sp) || !vk_mem_span_live(&sp)) return up_why = UP_NOT_LIVE, 0;
int one_d = e->pub.type == VK_IMAGE_TYPE_1D, is3d = d->target == NVN_TEXTURE_TARGET_3D;
uint32_t levels = e->pub.levels, layers = e->pub.layers, n = levels * layers, k = 0;
VkDeswzRegion *dr = calloc(n, sizeof *dr);
VkBufferImageCopy *rg = calloc(n, sizeof *rg);
VkAstcRegion *ar = agpu ? calloc(n, sizeof *ar) : NULL;
if (!dr || !rg || (agpu && !ar)) rt_fatal("vk: out of memory (texture upload regions)");
int ok = 1;
VkDeviceSize max_dst = DESWZ_CHUNK;
for (uint32_t lv = 0; ok && lv < levels; lv++)
for (uint32_t ly = 0; ok && ly < layers; ly++) {
uint32_t row_bytes, rows, slices, gbpb;
uint64_t base;
unsigned th, td;
uint32_t w = mip(d->width, lv), h = one_d ? 1 : mip(d->height, lv), sl = is3d ? mip(d->depth, lv) : 1;
if (nvn_deswizzle_geom(d, lv, ly, &row_bytes, &rows, &slices, &base, &th, &td, &gbpb) || gbpb != (uint32_t)bpb ||
base >= t->L.size || rows != (h + bh - 1) / bh || slices != sl) {
ok = 0;
break;
}
uint64_t bhg = 1ull << th, bdg = 1ull << td, gobs_x = (row_bytes + 63) / 64;
uint64_t blocks_y = (rows + 8 * bhg - 1) / (8 * bhg), blocks_z = (slices + bdg - 1) / bdg;
uint64_t segs = gobs_x * blocks_y * blocks_z * (32ull << (th + td)), room = (t->L.size - base) / 16;
if (segs > room) segs = room;
uint32_t pitch16 = (row_bytes + 15) / 16;
VkDeviceSize out = (VkDeviceSize)pitch16 * 16 * rows * slices;
if (segs > UINT32_MAX || gobs_x * blocks_y > UINT32_MAX) { ok = 0; break; }
if (out > max_dst) max_dst = out;
dr[k] = (VkDeswzRegion){.src = base, .segs = (uint32_t)segs, .pitch16 = pitch16, .rows = rows, .slices = slices,
.th = th, .td = td, .gobs_x = (uint32_t)gobs_x, .blocks_y = (uint32_t)blocks_y};
rg[k] = (VkBufferImageCopy){.bufferRowLength = pitch16 * 16 / (uint32_t)bpb * (uint32_t)bw,
.bufferImageHeight = rows * (uint32_t)bh,
.imageSubresource = {e->pub.aspect, lv, ly, 1}, .imageExtent = {w, h, sl}};
if (agpu) ar[k] = (VkAstcRegion){.w = w, .h = h, .slices = sl, .level = lv, .layer = ly};
k++;
}
if (!ok || k != n || !vk_deswz_fits(sp.offset, t->L.size, dr, (int)n, max_dst)) {
free(dr), free(rg), free(ar);
return up_why = !ok || k != n ? UP_GEOM : UP_LIMITS, 0;
}
VkCtx *c = vk_ctx();
VkCommandBuffer cb = vk_upload_begin();
// Earlier GPU writes into the pool (copies, shaders) before the reads; the image's earlier uses before its copy.
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT,
VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
for (uint32_t k0 = 0, k1; k0 < n; k0 = k1) {
VkDeviceSize size = 0;
for (k1 = k0; k1 < n; k1++) {
VkDeviceSize out = (VkDeviceSize)dr[k1].pitch16 * 16 * dr[k1].rows * dr[k1].slices;
if (k1 > k0 && size + out > DESWZ_CHUNK) break;
dr[k1].dst = size, rg[k1].bufferOffset = size;
if (agpu) ar[k1].src = size;
size += out;
}
VkBuffer sb;
VmaAllocation sa;
if (vk_scratch_buffer(size, &sb, &sa) != VK_SUCCESS) {
log_limited(&n_scratch_fail, "texture %#llx: deswizzle scratch of %llu bytes failed%s",
(unsigned long long)e->pub.addr, (unsigned long long)size,
k0 ? "; its later levels left as they were" : "; deswizzled on the CPU");
if (!k0) {
vk_upload_end();
free(dr), free(rg), free(ar);
return up_why = UP_SCRATCH, 0;
}
break;
}
if (!vk_deswz_record(cb, sp.buffer, sp.offset, t->L.size, sb, size, dr + k0, (int)(k1 - k0))) {
log_limited(&n_bad, "texture %#llx: deswizzle of levels %u-%u not recorded; left as they were",
(unsigned long long)e->pub.addr, k0, k1 - 1); // vk_deswz_fits passed: not expected
vk_defer_buffer(sb, sa);
break;
}
barrier(cb, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
if (agpu) {
uint64_t a0 = perf_begin();
if (!vk_astc_record(cb, sb, size, ar + k0, (int)(k1 - k0), e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb,
e->f.astc_bc, e->pub.image, e->pub.aspect, VK_NULL_HANDLE))
log_limited(&n_undecoded, "ASTC texture %#llx (fmt %#x %ux%u) not decoded on the GPU: left as it was",
(unsigned long long)e->pub.addr, d->format, d->width, d->height);
perf_end(PERF_ASTC, a0);
} else {
vkCmdCopyBufferToImage(cb, sb, e->pub.image, VK_IMAGE_LAYOUT_GENERAL, k1 - k0, rg + k0);
}
if (mode == VK_DESWZ_CHECK) { // the linear copy back to the host, compared with the CPU deswizzle
VkBuffer rb;
VmaAllocation ra;
const uint8_t *rmap = vk_staging(size, &rb, &ra);
VkBufferCopy cp = {0, 0, size};
vkCmdCopyBuffer(cb, sb, rb, 1, &cp);
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT,
VK_ACCESS_2_HOST_READ_BIT);
vk_defer_buffer(sb, sa);
vk_upload_end();
vk_upload_flush(1);
vmaInvalidateAllocation(c->vma, ra, 0, VK_WHOLE_SIZE);
for (uint32_t i = k0; i < k1; i++) {
const VkDeswzRegion *r = &dr[i];
size_t pitch = (size_t)r->pitch16 * 16, bytes = pitch * r->rows * r->slices;
uint8_t *cpu = malloc(bytes);
if (!cpu) rt_fatal("vk: out of memory (deswizzle check)");
uint32_t row_bytes = 0, rows, slices, gbpb;
uint64_t base;
unsigned th, td;
nvn_deswizzle_geom(d, rg[i].imageSubresource.mipLevel, rg[i].imageSubresource.baseArrayLayer, &row_bytes,
&rows, &slices, &base, &th, &td, &gbpb);
nvn_deswizzle(d, rg[i].imageSubresource.mipLevel, rg[i].imageSubresource.baseArrayLayer, t->mem, cpu, pitch);
uint64_t bad = 0, first = UINT64_MAX;
for (uint64_t row = 0; row < (uint64_t)r->rows * r->slices; row++)
if (memcmp(cpu + row * pitch, rmap + r->dst + row * pitch, row_bytes)) {
bad++;
if (first == UINT64_MAX) first = row;
}
free(cpu);
plat_add64(&n_checked, 1);
if (bad) {
plat_add64(&n_differ, 1);
rt_log("vk: deswizzle check: texture %#llx fmt %#x %ux%u level %u layer %u: %llu of %u rows differ, the "
"first %llu (th %u td %u, %u rows of %u bytes, %u slices)", (unsigned long long)e->pub.addr,
d->format, d->width, d->height, rg[i].imageSubresource.mipLevel,
rg[i].imageSubresource.baseArrayLayer, (unsigned long long)bad, r->rows * r->slices,
(unsigned long long)first, r->th, r->td, r->rows, row_bytes, r->slices);
}
}
vmaDestroyBuffer(c->vma, rb, ra);
if ((n_checked & 1023) == 0 || n_checked < 2)
rt_log("vk: deswizzle check: %lld regions compared, %lld differ", (long long)n_checked, (long long)n_differ);
cb = vk_upload_begin();
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT,
VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
continue;
}
vk_defer_buffer(sb, sa);
if (k1 < n && vk_upload_held() > UPLOAD_HELD_MAX) { // submit what is recorded, go on in a new batch
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, ALL_RW);
vk_upload_end();
vk_upload_flush(0);
cb = vk_upload_begin();
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT,
VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
}
}
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, ALL_RW);
vk_upload_end();
if (vk_upload_held() > UPLOAD_HELD_MAX) vk_upload_flush(0);
free(dr), free(rg), free(ar);
return 1;
}
// Copies the whole storage into the image (every level and layer the image has): on the GPU where it can
// (upload_gpu), else on the CPU.
static void upload(Tex *e) {
const tex_t *t = &e->t;
const nvn_texdesc *d = &t->d;
int bw, bh;
if (!nvn_fmt_bpb(d->format, &bw, &bh) || !t->mem) return;
uint64_t p0 = perf_begin(), u0 = kern_now_ns();
int why = UP_GPU;
if (!upload_gpu(e)) why = up_why, upload_cpu(e);
uint64_t dt = kern_now_ns() - u0;
up_n[why]++, up_bytes[why] += t->L.size, up_ns[why] += dt;
if (e->uploaded) {
n_reuploads++, perf_count(PERF_CNT_TEX_REUP);
ru_n++, ru_bytes += t->L.size, ru_ns += dt;
if (e->wt_n) {
ru_wt_n++, ru_wt_bytes += t->L.size, ru_wt_ns += dt;
if (rt_trace & RT_TRACE_PERF)
rt_log("perf: frame %llu: re-upload after %u WriteTexels: texture %#llx fmt %#x %ux%u, %.1f KiB, %.3f ms",
(unsigned long long)__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), e->wt_n,
(unsigned long long)e->pub.addr, d->format, d->width, d->height, t->L.size / 1024.0, dt / 1e6);
}
} else {
n_uploads++, perf_count(PERF_CNT_TEX_UP);
}
e->wt_n = 0;
e->uploaded = 1;
e->n_up++;
perf_end(PERF_TEX, p0);
}
// Whether e's storage fills no 4 KiB page of its own, so the write tracking can only see writes to pages it shares.
static int fills_no_page(const Tex *e) {
return ((e->pub.addr + e->t.L.size) & ~0xfffull) <= ((e->pub.addr + 0xfff) & ~0xfffull);
}
// A texture that fills no page keeps a copy of its storage bytes from when a GPU write to it was recorded
// (vk_texture_gpu_written), up to SNAP_MAX bytes; wb_exact (TOTK_WB_EXACT, default on) enables it.
enum { SNAP_MAX = 4096 };
static int wb_exact = -1;
static volatile int32_t n_kept;
// vk_mem_changed over the pages wholly inside e's storage (all of its pages when it fills none): a neighbour that shares
// the first or last page does not count as a write to e. A texture that fills no page shares all of its pages, so
// there a page write counts only when e's own bytes differ from the copy taken at the GPU write. TotK's Purah Pad
// camera renders the ID, screen rect and depth of what is under the reticle into 4x4 targets of 512 bytes, two
// slots used in turn, and the CPU reads them a frame later. One slot's rect target shares its page with data the CPU
// writes every frame: without the copy its renders were never written back, the game read a zero rect every other
// frame and the target's name label blinked. TOTK_WB_EXACT=0 restores the page-only decision.
static int storage_rewritten(const Tex *e, uint32_t since, uint32_t *now) {
uint64_t lo = (e->pub.addr + 0xfff) & ~0xfffull, hi = (e->pub.addr + e->t.L.size) & ~0xfffull;
if (hi <= lo) lo = e->pub.addr, hi = e->pub.addr + e->t.L.size;
int ch = vk_mem_changed(lo, hi - lo, since, now);
if (ch != 1 || !since || !e->snap || memcmp(e->snap, (const void *)(uintptr_t)e->pub.addr, e->t.L.size)) return ch == 1;
log_limited(&n_kept, "write-back of %#llx kept: fmt %#x %ux%u, %llu bytes; a neighbour wrote its page",
(unsigned long long)e->pub.addr, e->t.d.format, e->t.d.width, e->t.d.height,
(unsigned long long)e->t.L.size);
return 0;
}
// A mirrored texture is uploaded on first use and again when its storage pages carry a newer write generation.
static void make_fresh(Tex *e) {
if (!e->pub.mirrored || e->pub.gpu_owned) return;
uint64_t size = e->t.L.size;
if (!e->uploaded) {
vk_mem_changed(e->pub.addr, size, 0, &e->wgen); // resets the write bits before the storage is read
upload(e);
return;
}
uint32_t now;
int ch = vk_mem_changed(e->pub.addr, size, e->wgen, &now);
e->wgen = now;
e->last_ch = ch;
if (ch) upload(e); // 1 written, -1 not tracked: both upload again
}
// make_fresh for draw bindings: the page check answers from this submission's write-watch snapshot (vk_memory.c
// vk_mem_changed_snap) instead of a system call per texture. A write the snapshot of the texture's region missed (made
// after it, during this replay) keeps its write-watch bit and is caught by the next submission's snapshot.
static void make_fresh_snap(Tex *e) {
if (!e->pub.mirrored || e->pub.gpu_owned) return;
uint32_t now;
n_checks++;
int ch = vk_mem_changed_snap(e->pub.addr, e->t.L.size, e->uploaded ? e->wgen : 0, submit_epoch, &now);
e->wgen = now;
if (!e->uploaded) { upload(e); return; }
e->last_ch = ch;
if (ch) upload(e);
}
// ---------------------------------------------------------------- aliases (TOTK_TEX_ALIAS)
// Bytes per block and block size of the image's VkFormat (ASTC decodes to RGBA8, RGB expands to RGBA); 0 when the
// VkFormat substitutes another one of unknown size.
static int img_block(const Tex *e, int *bw, int *bh) {
if (e->f.astc_bc) { *bw = *bh = 4; return 16; }
if (e->f.astc_bw) { *bw = *bh = 1; return 4; }
int b = nvn_fmt_bpb(e->t.d.format, bw, bh);
if (e->f.expand_rgb) { *bw = *bh = 1; return b / 3 * 4; }
return e->f.vk == e->f.ideal ? b : 0;
}
// The new entry e can take src's content texel for texel: colour, one sample, the same shape and storage layout, and
// image formats of one block size.
static int alias_compatible(const Tex *e, const Tex *src) {
const VkTexImage *p = &e->pub, *q = &src->pub;
if (e->f.depth || e->f.stencil || src->f.depth || src->f.stencil || p->samples > 1 || q->samples > 1) return 0;
if (p->type != q->type || p->width != q->width || p->height != q->height || p->depth != q->depth) return 0;
if (e->t.L.size != src->t.L.size || nvn_tex_is_linear(&e->t.d) != nvn_tex_is_linear(&src->t.d)) return 0;
int ew, eh, sw, sh, eb = img_block(e, &ew, &eh), sb = img_block(src, &sw, &sh);
return eb && eb == sb && ew == sw && eh == sh;
}
// Whether e is live at op pos of a chunked replay: the state scan is ahead of the chunks, and an entry it revived at a
// later op is still released for this one. Two 4K targets at one address, RGBA8 and R8 with memory of their own, were
// swapped whenever the scan got past the second's SET_RT before a chunk sampling the storage got its turn, and large
// parts of the scene flickered. UINT32_MAX: the state now.
static int live_at(const Tex *e, uint32_t pos) {
return !e->released && !(e->rev_epoch == submit_epoch && e->rev_pos > pos);
}
// src holds content the storage bytes do not, at op pos of a chunked replay (UINT32_MAX: the state now): the GPU wrote
// it and, for a mirrored texture, it was not written back and the CPU has not rewritten the storage since (the new
// entry's upload then reads the newer bytes). A chunk at a later op that rendered src first does not count: in replay
// order that render comes after the new entry's first use.
static int alias_has_gpu_content(const Tex *src, uint32_t pos) {
if (!src->pub.gpu_owned || (src->own_epoch == submit_epoch && src->own_pos > pos)) return 0;
if (!src->pub.mirrored) return 1;
uint32_t now;
return src->gpu_dirty && !storage_rewritten(src, src->gpu_gen, &now);
}
// Maxwell reads the same bytes through both descriptors: src's content goes into the new image e at the replay's
// current position, before any later command of the submission uses e. Through a buffer, since bound images share
// their memory and image copies may not overlap; e leaves UNDEFINED between the two halves, so its first transition
// cannot clobber src's bytes before they are read. 0 when no replay is recording (e is then not initialised yet).
static int alias_copy(Tex *e, const Tex *src) {
int reopen;
VkCommandBuffer cb = vk_exec_inline_begin(&reopen);
if (!cb) return 0;
int bw, bh, bpb = img_block(e, &bw, &bh);
uint32_t levels = e->pub.levels < src->pub.levels ? e->pub.levels : src->pub.levels;
uint32_t layers = e->pub.layers < src->pub.layers ? e->pub.layers : src->pub.layers, n = levels * layers, k = 0;
VkBufferImageCopy *rg = calloc(n, sizeof *rg);
if (!rg) rt_fatal("vk: out of memory for an alias copy");
VkDeviceSize total = 0;
for (uint32_t lv = 0; lv < levels; lv++)
for (uint32_t ly = 0; ly < layers; ly++, k++) {
uint32_t w = mip(e->pub.width, lv), h = mip(e->pub.height, lv), z = mip(e->pub.depth, lv);
rg[k] = (VkBufferImageCopy){.bufferOffset = total, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, lv, ly, 1},
.imageExtent = {w, h, z}};
total += ((VkDeviceSize)(w + bw - 1) / bw * ((h + bh - 1) / bh) * z * bpb + 15) & ~15ull;
}
VkBuffer buf = vk_exec_inline_buffer(total);
if (!buf) {
free(rg);
vk_exec_inline_end(reopen);
return 0;
}
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
vkCmdCopyImageToBuffer(cb, src->pub.image, VK_IMAGE_LAYOUT_GENERAL, buf, n, rg);
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT};
VkImageMemoryBarrier2 ib = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = e->pub.image,
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS}};
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb,
.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &ib};
vkCmdPipelineBarrier2(cb, &di);
vkCmdCopyBufferToImage(cb, buf, e->pub.image, VK_IMAGE_LAYOUT_GENERAL, n, rg);
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
vk_exec_inline_end(reopen);
free(rg);
return 1;
}
static void entry_live(Tex *e) {
if (!e->released) return;
takeback_n[e->pub.gpu_owned != 0][age_bucket(coarse_ms - e->rel_ms)]++;
e->use_seq = ++use_counter;
e->released = 0, nreleased--, rel_unlink(e), tex_gen_bump(e->key.addr);
}
// A live colour entry at e's storage of e's size, as of op pos: the new entry may take its content (alias_copy).
static int alias_candidate(const TexKey *k, const tex_t *t, uint32_t pos) {
for (const Tex *p = nvn_hm_get(&by_addr, &k->addr); p; p = p->next_at)
if (live_at(p, pos) && !p->f.depth && !p->f.stencil && p->t.L.size == t->L.size) return 1;
return 0;
}
static Tex *entry_of(const tex_t *t) {
TexKey k;
make_key(t, &k);
Tex *e = nvn_hm_get(&by_key, &k);
uint32_t pos = vk_exec_replay_pos();
// A chunk of a chunked replay makes the entry as of its own op: whether it takes the content of a texture at its
// storage (alias_copy) depends on what the chunks before it render there, and those may still be recording on
// other threads. Taken from a render they had not recorded yet, the new image got the storage's old bytes instead:
// on an RX 7800 XT the grass near Link was missing in 3 of 29 launches with 4 threads (a map texture drawn once
// per tile, kept from then on). With a live colour texture of its size at the storage, the chunk waits until the
// chunks before it are recorded, the texture lock released for the wait (as often as this thread holds it).
if (!e && pos != UINT32_MAX && tex_alias != 0 && alias_candidate(&k, t, pos) && vk_exec_chunk_behind()) {
tex_t own = *t; // t may point into tslots, which a registration moves while the lock is released
t = &own;
int d = tex_depth;
for (int i = 0; i < d; i++) unlock();
vk_exec_chunk_settle();
for (int i = 0; i < d; i++) lock();
tex_settles++;
if ((e = nvn_hm_get(&by_key, &k))) { // another chunk made it meanwhile
entry_live(e);
e->t = *t;
return e;
}
}
if (!e) {
if (tex_alias < 0) {
tex_alias = cfg_bool("TOTK_TEX_ALIAS");
rt_log("vk: texture aliases: %s", tex_alias ? "a new image takes the live image's content" : "a new image starts empty");
}
if (!(e = create_tex(t, &k, tex_alias))) return NULL;
perf_count(PERF_CNT_TEX_NEW);
dropped_check(&k);
e->use_seq = ++use_counter;
// released entries of another descriptor at this address are replaced by the new one; a live one holding
// GPU-written content is the alias source, preferring the one that differs only in swizzle (same_but_swizzle)
int alias = 0;
Tex *src = NULL;
for (Tex *p = nvn_hm_get(&by_addr, &k.addr), *nx; p; p = nx) {
nx = p->next_at;
if (p->released) { destroy_tex(p); continue; }
alias = 1;
if (!live_at(p, pos)) continue; // the scan revives it at a later op: released at this chunk's
if (tex_alias && (!src || (same_but_swizzle(&p->key, &k) && !same_but_swizzle(&src->key, &k))) &&
alias_compatible(e, p) && alias_has_gpu_content(p, pos))
src = p;
}
if (alias) perf_count(PERF_CNT_TEX_ALIAS);
if (tex_alias) {
if (src && alias_copy(e, src)) {
n_alias_copied++;
if (e->pub.mirrored) { // the content is src's GPU render, newer than the storage bytes
vk_mem_changed(e->pub.addr, e->t.L.size, 0, &e->wgen);
e->uploaded = 1;
e->pub.gpu_owned = 1;
}
} else {
init_image(e, vk_upload_begin());
vk_upload_end();
if (alias) {
if (e->pub.mirrored) n_alias_upload++;
else n_alias_uninit++;
}
}
}
e->next_at = nvn_hm_get(&by_addr, &k.addr);
nvn_hm_put(&by_addr, &k.addr, e);
nvn_hm_put(&by_key, &k, e);
tex_gen_bump(k.addr);
}
entry_live(e);
e->t = *t;
return e;
}
// vk_dump: upload bookkeeping of an image vk_texture_get / vk_texture_by_id returned. pages_written: whether its
// storage pages were written since the last check (1, 0, -1 not tracked); the query leaves that check's state alone.
void vk_texture_debug(const VkTexImage *img, uint32_t *uploads, int *last_change, int *pages_written) {
lock();
const Tex *e = (const Tex *)img; // pub is the first member
uint32_t now;
*uploads = e->n_up;
*last_change = e->last_ch;
*pages_written = e->pub.mirrored && e->uploaded ? vk_mem_changed(e->pub.addr, e->t.L.size, e->wgen, &now) : 0;
unlock();
}
const VkTexImage *vk_texture_get(const tex_t *t) {
if (!t || !t->mem) return NULL;
lock();
Tex *e = entry_of(t);
if (e) make_fresh(e);
unlock();
return e ? &e->pub : NULL;
}
// The replay's state scan (vk_exec.c): the entry vk_texture_get would return, neither created nor made current; the
// recording does that. It is revived here, in replay order, and remembers the op it happened at: a chunk may bind a
// finalized target's slot before the chunk that renders it reached its SET_RT, or long after the scan went past a later
// target at the same storage, and slot_resolve has to answer as the replay would at the chunk's own op (live_at).
const VkTexImage *vk_texture_revive(const tex_t *t, uint32_t pos) {
if (!t || !t->mem) return NULL;
TexKey k;
make_key(t, &k);
lock();
Tex *e = nvn_hm_get(&by_key, &k);
if (e && e->released) {
e->rev_epoch = submit_epoch, e->rev_pos = pos;
__atomic_store_n(&arev[agen_idx(k.addr)], submit_epoch, __ATOMIC_RELEASE);
entry_live(e);
}
unlock();
return e ? &e->pub : NULL;
}
// Render targets the CPU reads without NVN's invalidate (vk_texture_write_back_small); small_pending: one may be dirty.
// wb_limit: targets up to this size (bytes) always go back when the CPU waits for a fence, TOTK_WB_FENCE_LIMIT, read
// once. A larger one goes back unless the GPU wrote it in each of the last WB_RUN frames (wb_wanted). wb_trace:
// TOTK_TRACE=gfx, read together with it.
enum { WB_SMALL_DEFAULT = 64 << 10, WB_RUN = 16 };
static int small_pending;
static uint64_t wb_limit = WB_SMALL_DEFAULT;
static int wb_trace;
static int wb_cfg_read;
static void wb_cfg_init(void) {
if (wb_cfg_read) return;
int v = cfg_int("TOTK_WB_FENCE_LIMIT");
wb_limit = v < 0 ? 0 : (uint64_t)v;
wb_trace = (rt_trace & RT_TRACE_GFX) != 0;
wb_cfg_read = 1;
if (wb_limit != WB_SMALL_DEFAULT) rt_log("vk: fence write-back limit %llu bytes", (unsigned long long)wb_limit);
}
// Whether the fence write-back takes a dirty target. A target the GPU draws every frame is scratch the CPU does not
// read: in Hyrule Field three 256x256 RGBA8 targets and a 256x256 RG8 one, which cost 2.7 ms a frame to write back.
// TotK's 260x260 RG8 grass maps, which its fire spread reads after a fence wait (#132), are drawn once per change, or
// up to 9 frames in a row while a tile loads.
static int wb_wanted(const Tex *e) { return e->t.L.size <= wb_limit || e->run < WB_RUN; }
void vk_texture_gpu_written(const tex_t *t) {
if (!t || !t->mem) return;
vk_exec_frame_written(t);
lock();
Tex *e = entry_of(t);
if (e) {
make_fresh(e); // a partial render keeps what the storage held before
if (!e->pub.gpu_owned) { // entry_of's alias choice of a chunk at an earlier op leaves this render out
uint32_t pos = vk_exec_replay_pos();
e->own_epoch = pos == UINT32_MAX ? 0 : submit_epoch, e->own_pos = pos;
}
e->pub.gpu_owned = 1;
uint64_t f = vk_exec_recording_fence();
if (f > e->wr_fence) e->wr_fence = f;
if (e->pub.mirrored) {
e->gpu_dirty = 1;
storage_rewritten(e, 0, &e->gpu_gen); // CPU writes up to here are older than this render
if (wb_exact < 0) wb_exact = cfg_bool("TOTK_WB_EXACT");
if (wb_exact && !e->snap && e->t.L.size <= SNAP_MAX && fills_no_page(e) && !(e->snap = malloc(SNAP_MAX)))
rt_fatal("vk: out of memory for a texture's storage copy");
if (e->snap) memcpy(e->snap, (const void *)(uintptr_t)e->pub.addr, e->t.L.size);
e->gpu_seq = ++gpu_writes;
e->gpu_fence = vk_exec_recording_fence();
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
if (fr != e->run_frame || !e->run) {
e->run = fr == e->run_frame + 1 && e->run ? (uint8_t)(e->run < WB_RUN ? e->run + 1 : WB_RUN) : 1;
e->run_frame = fr;
}
wb_cfg_init();
if (wb_wanted(e)) __atomic_store_n(&small_pending, 1, __ATOMIC_RELEASE);
own_add(e);
}
}
unlock();
}
void vk_texture_release(const tex_t *t) {
if (!t || !t->mem) return;
TexKey k;
make_key(t, &k);
lock();
Tex *e = nvn_hm_get(&by_key, &k);
if (e && !e->released) {
e->released = 1;
e->rel_ms = coarse_ms;
e->rel_seq = use_counter;
nreleased++;
rel_append(e);
tex_gen_bump(k.addr);
sweep_released();
}
unlock();
}
void vk_texture_range_gone(uint64_t lo, uint64_t size, VmaAllocation alloc) {
lock();
Tex **list = malloc(sizeof *list * (size_t)(by_key.n + 1));
int n = 0;
for (uint32_t i = 0; i < by_key.cap; i++) {
Tex *e = by_key.vals[i];
if (e && ((e->key.addr >= lo && e->key.addr - lo < size) || (alloc && e->bound_to == alloc))) list[n++] = e;
}
for (int i = 0; i < n; i++) destroy_tex(list[i]);
free(list);
unlock();
}
// ---------------------------------------------------------------- descriptor slots
void vk_texture_register(int id, const tex_t *t, const view_t *v) {
if (id < 0 || id >= (1 << 20)) rt_fatal("vk: texture slot %d out of range", id);
lock();
if (id >= ntslots) {
int n = ntslots ? ntslots : 1024;
while (n <= id) n *= 2;
TexSlot *s = realloc(tslots, sizeof *s * (size_t)n);
if (!s) rt_fatal("vk: out of memory for texture slots");
memset(s + ntslots, 0, sizeof *s * (size_t)(n - ntslots));
tslots = s, ntslots = n;
}
if (!sinfo) {
uint64_t *p = calloc(1 << 20, sizeof *p);
uint32_t *q = calloc(1 << 20, sizeof *q);
if (!p || !q) rt_fatal("vk: out of memory for texture slot tokens");
__atomic_store_n(&sepoch, q, __ATOMIC_RELEASE);
__atomic_store_n(&sinfo, p, __ATOMIC_RELEASE);
}
TexSlot *s = &tslots[id];
s->t = *t;
s->used = 1;
s->ce = NULL;
slots_gen++;
if (!++reg_serial) reg_serial = 1;
__atomic_store_n(&sinfo[id], (uint64_t)reg_serial << 16 | agen_idx((uintptr_t)t->mem), __ATOMIC_RELEASE);
s->has_view = v != NULL;
if (v) s->v = *v;
else memset(&s->v, 0, sizeof s->v);
unlock();
}
// The entry slot s resolves to at op pos, and the view and has_view a caller gets with it (the slot's, with the
// descriptor's swizzle when the entry is another descriptor at the same storage). NULL when the slot names no texture.
static Tex *slot_resolve(TexSlot *s, view_t *view, int *has_view, uint32_t pos) {
*view = s->v;
*has_view = s->has_view;
TexKey k;
make_key(&s->t, &k);
Tex *e = nvn_hm_get(&by_key, &k);
if (!e || !live_at(e, pos)) { // MNG hle_nvn.c: a finalized slot follows the live texture at its storage
// Prefer the live entry that differs from the slot descriptor only in its swizzle: transient render targets
// share storage, and the newest one at the address is usually another size. The map screen samples its
// 2250x1080 map target through an (R,G,B,1) descriptor and followed a 960x60 target at the same address.
// Only an entry of the slot's texel format follows (sRGB aside): Maxwell reads the storage through the slot
// descriptor's format, and an entry of another format holds another texture's texels. The quick menu samples
// its 768x432 RGB10A2 scene capture through a slot whose entry was released; after a fuse the Autobuild
// thumbnail of the fused object, RGBA8 at the same storage, was the live entry, and the menu drew it behind
// the wheel (the object's silhouette, or garbled colours). Without such an entry the released one keeps the
// slot: its image holds what was last written through that descriptor.
Tex *own = e, *any = NULL;
e = NULL;
for (Tex *p = nvn_hm_get(&by_addr, &k.addr); p; p = p->next_at) {
if (!live_at(p, pos) || linear_format(p->key.d.format) != linear_format(k.d.format)) continue;
if (!any) any = p;
if (same_but_swizzle(&p->key, &k)) { e = p; break; }
}
if (!e) e = any ? any : own;
}
TexSlot copy; // entry_of may release the lock (a chunk's wait), and a registration then moves tslots
if (!e && s->t.mem) copy = *s, s = &copy, e = entry_of(&s->t);
// A followed texture is another descriptor at the same storage: its image holds the content, but sampling takes the
// slot descriptor's swizzle. The UI renders text into RGBA8 and samples it through a (A,A,A,A) descriptor; with the
// render target's RGBA swizzle the text blur saw R = 1 everywhere and drew solid boxes behind tips and labels.
if (e && !(s->has_view && s->v.set_swizzle)) {
const nvn_texdesc *d = &s->t.d;
uint8_t sw[4] = {(uint8_t)d->swizzle_r, (uint8_t)d->swizzle_g, (uint8_t)d->swizzle_b, (uint8_t)d->swizzle_a};
if (!(sw[0] | sw[1] | sw[2] | sw[3])) // descriptors built without nvnTextureBuilderSetDefaults
sw[0] = NVN_TEXTURE_SWIZZLE_R, sw[1] = NVN_TEXTURE_SWIZZLE_G, sw[2] = NVN_TEXTURE_SWIZZLE_B,
sw[3] = NVN_TEXTURE_SWIZZLE_A;
if (memcmp(sw, e->pub.swz, 4)) {
if (!s->has_view) memset(view, 0, sizeof *view);
memcpy(view->swz, sw, 4);
view->set_swizzle = 1;
*has_view = 1;
}
}
return e;
}
void vk_texture_lock(void) { lock(); }
void vk_texture_unlock(void) { unlock(); }
uint32_t vk_texture_slots_gen(void) { return slots_gen; }
uint64_t vk_texture_slot_token(int id) {
const uint64_t *si = __atomic_load_n(&sinfo, __ATOMIC_ACQUIRE);
if (!si || id < 0 || id >= (1 << 20)) return 0;
uint64_t v = __atomic_load_n(&si[id], __ATOMIC_ACQUIRE);
return v ? (v >> 16) << 32 | __atomic_load_n(&agen[v & 0xffff], __ATOMIC_ACQUIRE) : 0;
}
uint32_t vk_sampler_gen(void) { return __atomic_load_n(&samp_gen, __ATOMIC_RELAXED); }
const VkTexImage *vk_texture_by_id(int id, view_t *view, int *has_view) {
lock();
const VkTexImage *r = vk_texture_by_id_locked(id, view, has_view);
unlock();
return r;
}
const VkTexImage *vk_texture_by_id_locked(int id, view_t *view, int *has_view) {
if (id < 0 || id >= ntslots || !tslots[id].used) return NULL;
TexSlot *s = &tslots[id];
uint32_t idx = sinfo[id] & 0xffff, g = agen[idx];
uint32_t pos = arev[idx] == submit_epoch ? vk_exec_replay_pos() : UINT32_MAX;
if (pos != UINT32_MAX) { // what the storage holds depends on the op: chunks at other ops share no answer in s
view_t v;
int hv;
Tex *f = slot_resolve(s, &v, &hv, pos);
s = &tslots[id]; // slot_resolve may have released the lock
if (f && (!f->uploaded || f->checked != submit_epoch)) {
make_fresh_snap(f);
f->checked = submit_epoch;
}
if (view) *view = has_view ? v : s->v;
if (has_view) *has_view = view ? hv : s->has_view;
return f ? &f->pub : NULL;
}
if (!s->ce || s->cgen != g) { // resolved again: an entry at its address was created, dropped or released, or the
view_t v; // slot registered (s->ce cleared); an empty result is not kept: image creation may
int hv; // succeed later
uint32_t settles = tex_settles;
Tex *r = slot_resolve(s, &v, &hv, UINT32_MAX); // entry_of may create an entry and bump g: g read before it
s = &tslots[id];
if (settles != tex_settles) { // the lock was released for a wait: the slot may be another texture by now
if (r && (!r->uploaded || r->checked != submit_epoch)) make_fresh_snap(r), r->checked = submit_epoch;
if (view) *view = has_view ? v : s->v;
if (has_view) *has_view = view ? hv : s->has_view;
return r ? &r->pub : NULL;
}
s->ce = r;
s->cview = v, s->chas_view = (uint8_t)hv;
s->cgen = g;
s->cepoch = 0;
__atomic_store_n(&sepoch[id], 0, __ATOMIC_RELAXED);
}
Tex *e = s->ce;
// Bindings of one submit share one check, answered from the submit's write-watch snapshot (guest writes during a
// replay race the GPU read on hardware too). After one check an entry is either uploaded or never uploads (not
// mirrored, GPU-owned or no CPU layout), so a slot that checked its entry in this submit has nothing left to do.
if (e && s->cepoch != submit_epoch) {
if (!e->uploaded || e->checked != submit_epoch) {
make_fresh_snap(e);
e->checked = submit_epoch;
}
s->cepoch = submit_epoch;
__atomic_store_n(&sepoch[id], submit_epoch, __ATOMIC_RELEASE);
}
if (view) *view = has_view ? s->cview : s->v;
if (has_view) *has_view = view ? s->chas_view : s->has_view;
return e ? &e->pub : NULL;
}
// vk_dump: the swizzle selectors of the descriptor registered in slot id; 0 when the slot is empty.
int vk_texture_slot_swz(int id, uint8_t swz[4]) {
lock();
int ok = id >= 0 && id < ntslots && tslots[id].used;
if (ok) {
const nvn_texdesc *d = &tslots[id].t.d;
swz[0] = (uint8_t)d->swizzle_r, swz[1] = (uint8_t)d->swizzle_g, swz[2] = (uint8_t)d->swizzle_b;
swz[3] = (uint8_t)d->swizzle_a;
}
unlock();
return ok;
}
uint32_t vk_texture_epoch(void) { return __atomic_load_n(&submit_epoch, __ATOMIC_RELAXED); }
int vk_texture_slot_moves(int id) {
const uint64_t *si = __atomic_load_n(&sinfo, __ATOMIC_ACQUIRE);
if (!si || id < 0 || id >= (1 << 20)) return 0;
uint64_t v = __atomic_load_n(&si[id], __ATOMIC_RELAXED);
return __atomic_load_n(&arev[v & 0xffff], __ATOMIC_ACQUIRE) == __atomic_load_n(&submit_epoch, __ATOMIC_RELAXED);
}
int vk_texture_slot_checked(int id) {
const uint32_t *e = __atomic_load_n(&sepoch, __ATOMIC_ACQUIRE);
return e && id >= 0 && id < (1 << 20) &&
__atomic_load_n(&e[id], __ATOMIC_ACQUIRE) == __atomic_load_n(&submit_epoch, __ATOMIC_RELAXED);
}
void vk_texture_new_submit(void) {
lock();
if (!++submit_epoch) submit_epoch = 1;
coarse_ms = plat_now_ms();
sweep_pressure();
if (__atomic_load_n(&rt_oom_req, __ATOMIC_RELAXED) && __atomic_exchange_n(&rt_oom_req, 0, __ATOMIC_ACQUIRE))
oom_release();
static uint64_t log_at;
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) / 60;
if ((rt_trace & RT_TRACE_PERF) && f != log_at && tex_alias > 0) {
log_at = f;
rt_log("perf: frame %llu: texture aliases: content copied %llu, filled from storage %llu, left uninitialised %llu",
(unsigned long long)f * 60, (unsigned long long)n_alias_copied, (unsigned long long)n_alias_upload,
(unsigned long long)n_alias_uninit);
n_alias_copied = n_alias_upload = n_alias_uninit = 0;
rt_log("perf: frame %llu: texture re-uploads %llu (%.1f MiB, %.2f ms), after WriteTexels %llu (%.1f MiB, %.2f ms)",
(unsigned long long)f * 60, (unsigned long long)ru_n, ru_bytes / 1048576.0, ru_ns / 1e6,
(unsigned long long)ru_wt_n, ru_wt_bytes / 1048576.0, ru_wt_ns / 1e6);
ru_n = ru_bytes = ru_ns = ru_wt_n = ru_wt_bytes = ru_wt_ns = 0;
char ub[512];
size_t uo = 0;
for (int i = 0; i < UP_WHY; i++)
if (up_n[i] && uo < sizeof ub)
uo += (size_t)snprintf(ub + uo, sizeof ub - uo, "%s %s %llu (%.1f MiB, %.2f ms)", uo ? "," : "",
up_why_name[i], (unsigned long long)up_n[i], up_bytes[i] / 1048576.0, up_ns[i] / 1e6);
if (uo) rt_log("perf: frame %llu: texture uploads by path:%s", (unsigned long long)f * 60, ub);
memset(up_n, 0, sizeof up_n), memset(up_bytes, 0, sizeof up_bytes), memset(up_ns, 0, sizeof up_ns);
uint64_t ck, calls, untr;
vk_mem_snap_stats(&ck, &calls, &untr);
rt_log("perf: frame %llu: binding storage checks %llu (texture %llu), write-watch snapshot queries %llu, untracked %llu",
(unsigned long long)f * 60, (unsigned long long)ck, (unsigned long long)n_checks, (unsigned long long)calls,
(unsigned long long)untr);
n_checks = 0;
rt_log("perf: frame %llu: released textures kept %d (%.0f MiB), dropped under VRAM pressure or the cap %llu "
"(%llu overwritten)", (unsigned long long)f * 60, nreleased, rel_bytes_total / 1048576.0,
(unsigned long long)n_pressure_drops,
(unsigned long long)n_pressure_overwritten);
n_pressure_drops = n_pressure_overwritten = 0;
}
unlock();
}
// ---------------------------------------------------------------- texel reads and writes
typedef struct { unsigned level, layer0; int nz, is3d, bw, bh, bpb; size_t wb, hb, row, img, x0, y0; } Region;
static int region_of(const tex_t *t, const view_t *v, const NVNcopyRegion *r, uint64_t row, uint64_t img, Region *g) {
g->bpb = nvn_fmt_bpb(t->d.format, &g->bw, &g->bh);
if (!g->bpb || r->width <= 0 || r->height <= 0 || r->depth < 0 || r->xoffset < 0 || r->yoffset < 0 || r->zoffset < 0 ||
r->xoffset % g->bw || r->yoffset % g->bh)
return 0;
g->level = v && v->set_levels ? v->base_level : 0;
g->layer0 = v && v->set_layers ? v->min_layer : 0;
g->nz = r->depth ? r->depth : 1;
g->is3d = t->d.target == NVN_TEXTURE_TARGET_3D;
g->wb = (size_t)((r->width + g->bw - 1) / g->bw) * (size_t)g->bpb;
g->hb = (size_t)(r->height + g->bh - 1) / (size_t)g->bh;
g->row = row ? row : g->wb;
g->img = img ? img : g->row * g->hb;
g->x0 = (size_t)(r->xoffset / g->bw) * (size_t)g->bpb;
g->y0 = (size_t)(r->yoffset / g->bh);
return 1;
}
// Pool bytes <-> caller rows of the region alone (nvn_texels_box). MacNativeGames' nvn_mtl.m goes through a
// linear copy of the whole level; for a small region of a large texture that cost tens of milliseconds while the guest
// thread held its virtual core and the caller the texture lock (monsters2 replay, frames 765-790).
static int storage_texels(const tex_t *t, const Region *g, const NVNcopyRegion *r, uint8_t *mem, int write) {
for (int z = 0; z < g->nz; z++) {
unsigned layer = g->is3d ? 0 : g->layer0 + (unsigned)r->zoffset + (unsigned)z;
uint32_t slice = g->is3d ? (uint32_t)(r->zoffset + z) : 0;
if (nvn_texels_box(&t->d, g->level, layer, (uint32_t)g->x0, (uint32_t)g->y0, slice, (uint32_t)g->wb,
(uint32_t)g->hb, t->mem, mem + (size_t)z * g->img, g->row, !write))
return 0;
}
return 1;
}
static int read_aspect(const Tex *e, VkImageAspectFlags *a) {
if (!e->f.depth && !e->f.stencil) { *a = VK_IMAGE_ASPECT_COLOR_BIT; return 1; }
switch (e->t.d.format) { // packed depth-stencil storage has no single-aspect copy
case 0x31: *a = VK_IMAGE_ASPECT_STENCIL_BIT; return 1;
case 0x32: case 0x34: *a = VK_IMAGE_ASPECT_DEPTH_BIT; return 1;
default: return 0;
}
}
// An image region read through a staging buffer. texels_read_record records the copy into the open upload batch;
// texels_read_take copies the rows out once that batch is done (vk_upload_flush(1)) and frees the buffer. Several
// records before one flush cost one GPU round trip instead of one each.
typedef struct {
VkBuffer sb;
VmaAllocation sa;
const uint8_t *stg;
size_t slice, ib; // ib: bytes per texel of an expanded RGB image, 0 when the image holds the storage layout
uint32_t w;
} TexRead;
static int texels_read_record(Tex *e, const Region *g, const NVNcopyRegion *r, TexRead *t) {
static volatile int32_t n_astc, n_ds;
VkImageAspectFlags aspect;
if (e->f.astc_bw) {
log_limited(&n_astc, "ReadTexels of ASTC texture %#llx: the image holds decoded RGBA8; zeros returned",
(unsigned long long)e->pub.addr);
return 0;
}
if (!read_aspect(e, &aspect)) {
log_limited(&n_ds, "texels of packed depth-stencil format %#x are not copied", e->t.d.format);
return 0;
}
if (g->level >= e->pub.levels) return 0;
uint32_t w = (uint32_t)r->width, h = (uint32_t)r->height;
t->w = w;
t->ib = e->f.expand_rgb ? (size_t)g->bpb / 3 * 4 : 0;
t->slice = t->ib ? (size_t)w * h * t->ib : g->wb * g->hb;
t->stg = vk_staging(t->slice * (size_t)g->nz, &t->sb, &t->sa);
VkBufferImageCopy *rg = calloc((size_t)g->nz, sizeof *rg);
if (!rg) rt_fatal("vk: out of memory for a texture read");
for (int z = 0; z < g->nz; z++)
rg[z] = (VkBufferImageCopy){.bufferOffset = t->slice * (size_t)z,
.imageSubresource = {aspect, g->level, g->is3d ? 0 : g->layer0 + (uint32_t)r->zoffset + (uint32_t)z, 1},
.imageOffset = {r->xoffset, r->yoffset, g->is3d ? r->zoffset + z : 0},
.imageExtent = {w, h, 1}};
VkCommandBuffer cb = vk_upload_begin();
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
vkCmdCopyImageToBuffer(cb, e->pub.image, VK_IMAGE_LAYOUT_GENERAL, t->sb, (uint32_t)g->nz, rg);
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT,
VK_ACCESS_2_HOST_READ_BIT);
vk_upload_end();
free(rg);
return 1;
}
static void texels_read_take(const Tex *e, const Region *g, const TexRead *t, uint8_t *mem) {
VkCtx *c = vk_ctx();
vmaInvalidateAllocation(c->vma, t->sa, 0, VK_WHOLE_SIZE);
for (int z = 0; z < g->nz; z++) {
const uint8_t *s = t->stg + t->slice * (size_t)z;
uint8_t *m = mem + (size_t)z * g->img;
for (size_t y = 0; y < g->hb; y++) {
if (t->ib) shrink_rgb(e->t.d.format, s + y * t->w * t->ib, m + y * g->row, t->w);
else memcpy(m + y * g->row, s + y * g->wb, g->wb);
}
}
vmaDestroyBuffer(c->vma, t->sb, t->sa);
}
// Image <-> caller rows through a staging buffer; the GPU copy is waited for.
static int image_texels(Tex *e, const Region *g, const NVNcopyRegion *r, uint8_t *mem, int write) {
static volatile int32_t n_astc, n_ds;
VkCtx *c = vk_ctx();
VkImageAspectFlags aspect;
if (!write) {
TexRead t;
if (!texels_read_record(e, g, r, &t)) return 0;
vk_upload_flush(1);
texels_read_take(e, g, &t, mem);
return 1;
}
if (!read_aspect(e, &aspect)) {
log_limited(&n_ds, "texels of packed depth-stencil format %#x are not copied", e->t.d.format);
return 0;
}
if (g->level >= e->pub.levels) return 0;
uint32_t w = (uint32_t)r->width, h = (uint32_t)r->height;
int rgb = e->f.expand_rgb, astc = e->f.astc_bw != 0, agpu = astc && vk_astc_gpu();
size_t texels = (size_t)w * h, ib = agpu ? 0 : astc ? 4 : rgb ? (size_t)g->bpb / 3 * 4 : 0;
size_t slice = ib ? texels * ib : g->wb * g->hb; // agpu: the raw blocks, decoded by vk_astc_record
VkBuffer sb;
VmaAllocation sa;
uint8_t *stg = vk_staging(slice * (size_t)g->nz, &sb, &sa);
VkBufferImageCopy *rg = calloc((size_t)g->nz, sizeof *rg);
for (int z = 0; z < g->nz; z++) {
uint8_t *s = stg + slice * (size_t)z;
const uint8_t *m = mem + (size_t)z * g->img;
if (agpu) {
for (size_t y = 0; y < g->hb; y++) memcpy(s + y * g->wb, m + y * g->row, g->wb);
} else if (astc) {
uint8_t *tight = malloc(g->wb * g->hb);
for (size_t y = 0; y < g->hb; y++) memcpy(tight + y * g->wb, m + y * g->row, g->wb);
uint64_t a0 = perf_begin();
astc_decode_ldr_mt(tight, w, h, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb, s);
perf_end(PERF_ASTC, a0);
free(tight);
} else if (rgb) {
for (size_t y = 0; y < g->hb; y++) expand_rgb(e->t.d.format, m + y * g->row, s + y * w * ib, w);
} else {
for (size_t y = 0; y < g->hb; y++) memcpy(s + y * g->wb, m + y * g->row, g->wb);
}
rg[z] = (VkBufferImageCopy){.bufferOffset = slice * (size_t)z,
.imageSubresource = {aspect, g->level, g->is3d ? 0 : g->layer0 + (uint32_t)r->zoffset + (uint32_t)z, 1},
.imageOffset = {r->xoffset, r->yoffset, g->is3d ? r->zoffset + z : 0},
.imageExtent = {w, h, 1}};
}
VkCommandBuffer cb = vk_upload_begin();
vmaFlushAllocation(c->vma, sa, 0, VK_WHOLE_SIZE);
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
int done = 0;
// A BC3 image takes whole 4x4 blocks, or a part that ends on the mip's edge; a region of a 5x5 or 6x6 footprint
// may end inside a block (vk_astc_record checks the start corner).
uint32_t mw = e->t.d.width >> g->level, mh = e->t.d.height >> g->level;
int bc_cut = e->f.astc_bc && (((w & 3) && (uint32_t)r->xoffset + w != (mw ? mw : 1)) ||
((h & 3) && (uint32_t)r->yoffset + h != (mh ? mh : 1)));
if (agpu && !bc_cut) { // bc_cut: nothing recorded, logged below as not decoded
VkAstcRegion *ar = calloc((size_t)g->nz, sizeof *ar);
for (int z = 0; z < g->nz; z++)
ar[z] = (VkAstcRegion){.src = slice * (size_t)z, .w = w, .h = h, .slices = 1, .level = rg[z].imageSubresource.mipLevel,
.layer = rg[z].imageSubresource.baseArrayLayer, .offset = rg[z].imageOffset};
uint64_t a0 = perf_begin();
done = vk_astc_record(cb, sb, slice * (size_t)g->nz, ar, g->nz, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb, e->f.astc_bc,
e->pub.image, aspect, VK_NULL_HANDLE);
perf_end(PERF_ASTC, a0);
free(ar);
}
if (!agpu) vkCmdCopyBufferToImage(cb, sb, e->pub.image, VK_IMAGE_LAYOUT_GENERAL, (uint32_t)g->nz, rg);
else if (!done) log_limited(&n_astc, "WriteTexels of ASTC texture %#llx not decoded on the GPU: left as it was",
(unsigned long long)e->pub.addr);
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
vk_upload_end();
vk_upload_flush(1);
free(rg);
vmaDestroyBuffer(c->vma, sb, sa);
return 1;
}
static void zero_rows(const Region *g, uint8_t *dst) {
for (int z = 0; z < g->nz; z++)
for (size_t y = 0; y < g->hb; y++) memset(dst + (size_t)z * g->img + y * g->row, 0, g->wb);
}
int vk_texture_read_image(const tex_t *t, const view_t *v, const NVNcopyRegion *r, uint8_t *dst, uint64_t row, uint64_t img) {
Region g;
if (!t || !region_of(t, v, r, row, img, &g)) return 0;
lock();
Tex *e = entry_of(t);
int ok = 0;
if (e) {
make_fresh(e);
ok = image_texels(e, &g, r, dst, 0);
}
unlock();
if (!ok) zero_rows(&g, dst);
return ok;
}
// ReadTexels: the pool bytes are the content of a mirrored texture the GPU has not rendered into; every other texture
// is read back from its image.
void vk_read_texels(const tex_t *t, const view_t *v, const NVNcopyRegion *r, uint8_t *dst, uint64_t row, uint64_t img) {
static volatile int32_t n_bad;
Region g;
if (!region_of(t, v, r, row, img, &g)) {
log_limited(&n_bad, "ReadTexels: invalid region %d,%d,%d %dx%dx%d (format %#x)", r->xoffset, r->yoffset,
r->zoffset, r->width, r->height, r->depth, t->d.format);
return;
}
lock();
Tex *e = entry_of(t);
int ok = 0;
if (e && e->pub.mirrored && !e->pub.gpu_owned) ok = storage_texels(t, &g, r, dst, 0);
else if (e) {
make_fresh(e);
ok = image_texels(e, &g, r, dst, 0);
}
unlock();
if (!ok) zero_rows(&g, dst);
}
// WriteTexels: a mirrored texture gets the texels in its pool bytes (and in its image once the GPU owns it); every
// other texture in its image.
void vk_write_texels(const tex_t *t, const view_t *v, const NVNcopyRegion *r, const uint8_t *src, uint64_t row, uint64_t img) {
static volatile int32_t n_bad;
Region g;
if (!region_of(t, v, r, row, img, &g) || (v && v->set_format && v->format != t->d.format)) {
log_limited(&n_bad, "WriteTexels: invalid region %d,%d,%d %dx%dx%d or view format (format %#x)", r->xoffset,
r->yoffset, r->zoffset, r->width, r->height, r->depth, t->d.format);
return;
}
lock();
Tex *e = entry_of(t);
if (e && e->pub.mirrored) {
if (!storage_texels(t, &g, r, (uint8_t *)src, 1))
log_limited(&n_bad, "WriteTexels: region outside level %u of texture %#llx", g.level, (unsigned long long)e->pub.addr);
vk_guest_written((uintptr_t)t->mem, t->L.size);
if (e->pub.gpu_owned) image_texels(e, &g, r, (uint8_t *)src, 1);
else if (e->uploaded) e->wt_n++;
} else if (e) {
image_texels(e, &g, r, (uint8_t *)src, 1);
}
unlock();
}
// ---------------------------------------------------------------- CPU reads of GPU-written storage
// A mirrored texture the GPU rendered into holds its texels in its image only; its storage keeps what the CPU last put
// there. The game may still read that storage with the CPU: TotK moves its generated grass maps between pools with
// memcpy once the GPU is done, and the copy then carried zeros. Such a read writes the image back first.
//
// Every level and layer of every texture in v is copied to staging in one upload batch with one wait: one GPU round
// trip per level and layer took 0.1 ms each, and a 260x260 grass map has 9 levels. The storage is then written in the
// order of v, so where two textures share storage the later one wins.
typedef struct {
Tex *e;
Region g;
NVNcopyRegion r;
TexRead t;
} WbPart;
static void write_back_n(Tex **v, int n) {
int np = 0, cap = 0;
WbPart *p = NULL;
for (int i = 0; i < n; i++) {
Tex *e = v[i];
const nvn_texdesc *d = &e->t.d;
int linear = nvn_tex_is_linear(d), is3d = d->target == NVN_TEXTURE_TARGET_3D;
uint32_t levels = linear ? 1 : e->pub.levels, layers = linear || is3d ? 1 : e->pub.layers;
for (uint32_t lv = 0; lv < levels; lv++)
for (uint32_t ly = 0; ly < layers; ly++) {
if (np == cap) {
cap = cap ? cap * 2 : 16;
if (!(p = realloc(p, sizeof *p * (size_t)cap))) rt_fatal("vk: out of memory for a texture write-back");
}
WbPart *q = &p[np];
view_t vw = {.set_levels = 1, .set_layers = 1, .base_level = (uint8_t)lv, .min_layer = (uint16_t)ly};
q->e = e;
q->r = (NVNcopyRegion){.width = (int)mip(d->width, lv),
.height = d->target == 0 || d->target == 3 ? 1 : (int)mip(d->height, lv),
.depth = is3d ? (int)mip(d->depth, lv) : 1};
if (region_of(&e->t, &vw, &q->r, 0, 0, &q->g) && texels_read_record(e, &q->g, &q->r, &q->t)) np++;
}
}
if (np) vk_upload_flush(1);
for (int i = 0; i < np; i++) {
WbPart *q = &p[i];
size_t bytes = q->g.img * (size_t)q->g.nz;
uint8_t *buf = malloc(bytes);
if (!buf) { // waits for memory with the texture lock held, after the release that needs no submit
vk_oom_release();
buf = rt_realloc_wait(NULL, bytes, "vk: texture write-back buffer");
}
if (!buf) rt_fatal("vk: out of memory for a texture write-back");
texels_read_take(q->e, &q->g, &q->t, buf);
storage_texels(&q->e->t, &q->g, &q->r, buf, 1);
free(buf);
}
free(p);
for (int i = 0; i < n; i++) v[i]->gpu_dirty = 0;
}
// Mirrored textures overlapping [addr, addr+size) that the GPU wrote since their last write-back.
static int owned_dirty(uint64_t addr, uint64_t size, Tex **hit) {
int n = 0;
for (int i = owned_lower(addr > max_owned ? addr - max_owned : 0); i < nowned && owned[i]->pub.addr < addr + size; i++) {
Tex *e = owned[i];
if (__atomic_load_n(&e->gpu_dirty, __ATOMIC_RELAXED) && e->pub.addr + e->t.L.size > addr) {
if (!hit) return 1;
hit[n++] = e;
}
}
return n;
}
// A storage image may be written by the shaders of the submission being recorded (texture lock held, vk_exec_draw.c
// tex_info). Its storage is not marked GPU-owned: only the write fence for ReadTexels.
void vk_texture_storage_bound_locked(const VkTexImage *img) {
Tex *e = (Tex *)img; // pub is the first member
uint64_t f = vk_exec_recording_fence();
if (f > e->wr_fence) e->wr_fence = f;
}
uint64_t vk_texture_last_write(const tex_t *t) {
lock();
Tex *e = entry_of(t);
uint64_t f = e ? e->wr_fence : 0;
unlock();
return f;
}
size_t vk_texture_texel_span(const tex_t *t, const view_t *v, const NVNcopyRegion *r, uint64_t row, uint64_t img) {
Region g;
if (!region_of(t, v, r, row, img, &g)) return 0;
return (size_t)(g.nz - 1) * g.img + (g.hb - 1) * g.row + g.wb;
}
int vk_texture_cpu_stale(uint64_t addr, uint64_t size) {
if (!size || !__atomic_load_n(&nowned, __ATOMIC_ACQUIRE)) return 0;
plat_mutex_lock_shared(&owned_lock);
int dirty = owned_dirty(addr, size, NULL);
plat_mutex_unlock_shared(&owned_lock);
return dirty;
}
// The newest submission recorded to write a GPU-written mirrored texture overlapping [addr, addr+size); 0: none.
uint64_t vk_texture_dirty_fence(uint64_t addr, uint64_t size) {
if (!size || !__atomic_load_n(&nowned, __ATOMIC_ACQUIRE)) return 0;
lock();
uint64_t f = 0;
for (int i = owned_lower(addr > max_owned ? addr - max_owned : 0); i < nowned && owned[i]->pub.addr < addr + size; i++) {
const Tex *e = owned[i];
if (!e->gpu_dirty || e->pub.addr + e->t.L.size <= addr) continue;
if (e->gpu_fence > f) f = e->gpu_fence;
if (e->wr_fence > f) f = e->wr_fence;
}
unlock();
return f;
}
// The image is the newest content only while nothing wrote the storage since the GPU render: TotK finalizes grass maps,
// reuses their storage for new data and memcpys that data on, and writing the old image back first handed the copy
// a stale map or stale resource bytes (wrong textures in the distance). Every hit is checked before any is written
// back, since a write-back is itself a CPU write to storage another hit may share; the hits then go back oldest render
// first, so where two descriptors share storage the newer render wins.
void vk_texture_write_back(uint64_t addr, uint64_t size) {
lock();
Tex **hit = malloc(sizeof *hit * (size_t)(nowned + 1));
if (!hit) rt_fatal("vk: out of memory for a texture write-back");
int n = owned_dirty(addr, size, hit), k = 0; // owned[] cannot change while tex_lock is held
for (int i = 0; i < n; i++) {
uint32_t now;
if (storage_rewritten(hit[i], hit[i]->gpu_gen, &now) == 1) hit[i]->gpu_dirty = 0;
else hit[k++] = hit[i];
}
for (int i = 1; i < k; i++)
for (int j = i; j > 0 && hit[j - 1]->gpu_seq > hit[j]->gpu_seq; j--) {
Tex *x = hit[j];
hit[j] = hit[j - 1], hit[j - 1] = x;
}
write_back_n(hit, k);
for (int i = 0; i < k; i++) perf_count(PERF_CNT_WB);
for (int i = k; i < n; i++) perf_count(PERF_CNT_WB_SKIP);
free(hit);
unlock();
}
// TotK's grass system renders a 128x128 RGBA8 map of cut and trampled grass into a CPU-cached pool every frame and its
// CPU code reads the pool bytes without nvnMemoryPoolInvalidateMappedRange (cut reactions: blades, insects, the cut
// itself). On the Switch the bytes are there once the frame's fence has passed. So when the
// CPU waits for a fence, mirrored targets rendered by work up to that fence go back to their storage (wb_wanted:
// targets over wb_limit only when not drawn every frame), unless the CPU rewrote it since. Targets drawn every frame
// still go back on an invalidate, memcpy or ReadTexels.
int vk_texture_small_dirty(void) { return __atomic_load_n(&small_pending, __ATOMIC_ACQUIRE); }
// TOTK_TRACE=gfx: a dirty target the fence write-back leaves alone because it is over the limit and drawn every frame
// (what a CPU read after the wait would miss), or sends back although it is over the default limit. One line per
// target address and outcome; the table is small and a full one stops logging.
static void wb_note(const Tex *e, int skipped) {
static uint64_t seen[2][64];
static int n_seen[2];
for (int i = 0; i < n_seen[skipped]; i++)
if (seen[skipped][i] == e->pub.addr) return;
if (n_seen[skipped] == 64) return;
seen[skipped][n_seen[skipped]++] = e->pub.addr;
const pool_t *p = e->t.pool;
unsigned fl = p ? p->flags : 0;
rt_log("vk: fence write-back %s %#llx %ux%u fmt %#x size %llu pool %s (limit %llu)",
skipped ? "skipped-drawn-every-frame" : "over-default", (unsigned long long)e->pub.addr, e->t.d.width, e->t.d.height,
e->t.d.format, (unsigned long long)e->t.L.size,
!p ? "none" : (fl & NVN_MEMORY_POOL_FLAGS_CPU_CACHED) ? "cpu-cached" : (fl & NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED) ? "cpu-uncached" : "other",
(unsigned long long)wb_limit);
}
void vk_texture_write_back_small(uint64_t fence) {
lock();
wb_cfg_init();
__atomic_store_n(&small_pending, 0, __ATOMIC_RELAXED);
plat_mutex_lock_shared(&owned_lock);
int n = 0, later = 0;
for (int i = 0; i < nowned; i++) n += owned[i]->gpu_dirty && wb_wanted(owned[i]);
Tex **hit = n ? malloc(sizeof *hit * (size_t)n) : NULL;
if (n && !hit) rt_fatal("vk: out of memory for a texture write-back");
n = 0;
for (int i = 0; i < nowned; i++) {
Tex *e = owned[i];
if (!e->gpu_dirty) continue;
if (!wb_wanted(e)) {
if (wb_trace && e->gpu_fence <= fence) {
wb_note(e, 1);
perf_count(PERF_CNT_WB_SKIP);
}
continue;
}
if (e->gpu_fence > fence) later = 1;
else {
if (wb_trace && e->t.L.size > WB_SMALL_DEFAULT) wb_note(e, 0);
hit[n++] = e;
}
}
plat_mutex_unlock_shared(&owned_lock);
if (later) __atomic_store_n(&small_pending, 1, __ATOMIC_RELEASE);
int k = 0;
for (int i = 0; i < n; i++) {
uint32_t now;
if (storage_rewritten(hit[i], hit[i]->gpu_gen, &now) == 1) hit[i]->gpu_dirty = 0;
else hit[k++] = hit[i];
}
for (int i = 1; i < k; i++)
for (int j = i; j > 0 && hit[j - 1]->gpu_seq > hit[j]->gpu_seq; j--) {
Tex *x = hit[j];
hit[j] = hit[j - 1], hit[j - 1] = x;
}
write_back_n(hit, k);
for (int i = 0; i < k; i++) perf_count(PERF_CNT_WB);
for (int i = k; i < n; i++) perf_count(PERF_CNT_WB_SKIP);
free(hit);
unlock();
}
// ---------------------------------------------------------------- samplers (LAS textures.c)
static VkCompareOp compare_op(uint32_t f) {
switch (f) {
case NVN_COMPARE_FUNC_NEVER: return VK_COMPARE_OP_NEVER;
case NVN_COMPARE_FUNC_LESS: return VK_COMPARE_OP_LESS;
case NVN_COMPARE_FUNC_EQUAL: return VK_COMPARE_OP_EQUAL;
case NVN_COMPARE_FUNC_LEQUAL: return VK_COMPARE_OP_LESS_OR_EQUAL;
case NVN_COMPARE_FUNC_GREATER: return VK_COMPARE_OP_GREATER;
case NVN_COMPARE_FUNC_NOTEQUAL: return VK_COMPARE_OP_NOT_EQUAL;
case NVN_COMPARE_FUNC_GEQUAL: return VK_COMPARE_OP_GREATER_OR_EQUAL;
default: return VK_COMPARE_OP_ALWAYS;
}
}
static struct { int init, mirror_clamp_edge, minmax, border_no_format; float aniso; } sf; // aniso: TOTK_ANISO, 0 = the game's
static void sampler_features(VkCtx *c) {
if (sf.init) return;
VkPhysicalDeviceCustomBorderColorFeaturesEXT cbf = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_FEATURES_EXT};
VkPhysicalDeviceVulkan12Features f12 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES,
.pNext = c->has_custom_border_color ? &cbf : NULL};
VkPhysicalDeviceFeatures2 f2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &f12};
vkGetPhysicalDeviceFeatures2(c->phys, &f2);
sf.mirror_clamp_edge = f12.samplerMirrorClampToEdge;
sf.minmax = f12.samplerFilterMinmax;
sf.border_no_format = c->has_custom_border_color && cbf.customBorderColors && cbf.customBorderColorWithoutFormat;
const char *an = cfg_value("TOTK_ANISO");
sf.aniso = strcmp(an, "game") ? (float)atoi(an) : 0;
rt_log("vk: anisotropic filtering: %s", sf.aniso > 1 ? an : "the game's");
sf.init = 1;
}
static VkSampler make_sampler(VkCtx *c, const samp_par *d) {
sampler_features(c);
VkSamplerCreateInfo si = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
si.magFilter = d->magf == NVN_MAG_FILTER_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST;
uint32_t mf = d->minf;
si.minFilter = mf == 1 || mf == 3 || mf == 5 ? VK_FILTER_LINEAR : VK_FILTER_NEAREST;
si.mipmapMode = mf == 4 || mf == 5 ? VK_SAMPLER_MIPMAP_MODE_LINEAR : VK_SAMPLER_MIPMAP_MODE_NEAREST;
VkSamplerAddressMode modes[3];
const uint8_t wrap[3] = {d->ws, d->wt, d->wr};
int border = 0;
for (int i = 0; i < 3; i++) {
switch (wrap[i]) {
case NVN_WRAP_MODE_CLAMP: case NVN_WRAP_MODE_CLAMP_TO_EDGE: modes[i] = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; break;
case NVN_WRAP_MODE_MIRROR_CLAMP: case NVN_WRAP_MODE_MIRROR_CLAMP_TO_EDGE:
modes[i] = sf.mirror_clamp_edge ? VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE : VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
break;
case NVN_WRAP_MODE_MIRROR_CLAMP_TO_BORDER: case NVN_WRAP_MODE_CLAMP_TO_BORDER:
modes[i] = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
border = 1;
break;
case NVN_WRAP_MODE_MIRRORED_REPEAT: modes[i] = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; break;
default: modes[i] = VK_SAMPLER_ADDRESS_MODE_REPEAT;
}
}
si.addressModeU = modes[0], si.addressModeV = modes[1], si.addressModeW = modes[2];
si.minLod = d->lod_min;
si.maxLod = d->lod_max > 1000 ? VK_LOD_CLAMP_NONE : d->lod_max;
if (mf <= NVN_MIN_FILTER_LINEAR) si.maxLod = si.minLod + 0.25f; // not mipmapped: stay on the base level
if (si.maxLod < si.minLod) si.maxLod = si.minLod;
float lim = c->props.limits.maxSamplerLodBias;
si.mipLodBias = fmaxf(-lim, fminf(lim, d->lod_bias));
// TOTK_ANISO raises the level for filtered, mipmapped colour samplers that repeat on U and V: the surface textures,
// which tile, so ground and walls seen at a shallow angle stay sharp into the distance. Depth-compare (shadow)
// samplers, nearest or unmipmapped ones (data maps, LUTs) and clamped ones keep the game's level: clamped samplers
// read screen-sized buffers, whose coordinates can stretch steeply at an object's edge, where anisotropic taps reach
// across it and leave a speckled fringe (the inventory's character preview).
float aniso = d->aniso;
// The game's own wrap modes: a mirror-clamp one maps to MIRRORED_REPEAT where the device lacks mirror-clamp-to-edge.
int tiled = (d->ws == NVN_WRAP_MODE_REPEAT || d->ws == NVN_WRAP_MODE_MIRRORED_REPEAT) &&
(d->wt == NVN_WRAP_MODE_REPEAT || d->wt == NVN_WRAP_MODE_MIRRORED_REPEAT);
if (sf.aniso > aniso && tiled && !d->cmp_mode && si.magFilter == VK_FILTER_LINEAR && si.minFilter == VK_FILTER_LINEAR &&
(mf == NVN_MIN_FILTER_LINEAR_MIPMAP_NEAREST || mf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR))
aniso = sf.aniso;
if (aniso > 1) {
si.anisotropyEnable = VK_TRUE;
si.maxAnisotropy = fminf(aniso, c->props.limits.maxSamplerAnisotropy);
}
if (d->cmp_mode) {
si.compareEnable = VK_TRUE;
si.compareOp = compare_op(d->cmp_func);
// A depth-compare sampler set to nearest returns 0 or 1 per tap, and the game's shaders smooth shadow edges by
// a few taps spaced for its own shadow map texels. With TOTK_SHADOW_SCALE those taps cover a fraction of the
// area and the edges turn hard and stepped; linear filtering makes every tap a 2x2 PCF instead.
if (shadow_scale() > 1) si.magFilter = si.minFilter = VK_FILTER_LINEAR;
}
VkSamplerCustomBorderColorCreateInfoEXT cb = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT};
VkSamplerReductionModeCreateInfo rm = {.sType = VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO};
const void **tail = &si.pNext;
if (border) {
const float *b = d->border;
if (b[0] == 0 && b[1] == 0 && b[2] == 0 && b[3] == 0) si.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
else if (b[0] == 0 && b[1] == 0 && b[2] == 0 && b[3] == 1) si.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
else if (b[0] == 1 && b[1] == 1 && b[2] == 1 && b[3] == 1) si.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
else if (sf.border_no_format) {
memcpy(cb.customBorderColor.float32, b, 16);
si.borderColor = VK_BORDER_COLOR_FLOAT_CUSTOM_EXT;
*tail = &cb;
tail = &cb.pNext;
} else si.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
}
if (d->reduction && sf.minmax && !si.compareEnable) {
rm.reductionMode = d->reduction == NVN_SAMPLER_REDUCTION_MIN ? VK_SAMPLER_REDUCTION_MODE_MIN : VK_SAMPLER_REDUCTION_MODE_MAX;
*tail = &rm;
}
VkSampler s;
VkResult r = vkCreateSampler(c->device, &si, NULL, &s);
if (r != VK_SUCCESS) rt_fatal("vk: vkCreateSampler failed (%d)", (int)r);
return s;
}
void vk_sampler_register(int id, const samp_par *s) {
if (id < 0 || id >= (1 << 12)) rt_fatal("vk: sampler slot %d out of range", id);
lock();
if (id >= nsslots) {
int n = nsslots ? nsslots : 256;
while (n <= id) n *= 2;
SampSlot *p = realloc(sslots, sizeof *p * (size_t)n);
if (!p) rt_fatal("vk: out of memory for sampler slots");
memset(p + nsslots, 0, sizeof *p * (size_t)(n - nsslots));
sslots = p, nsslots = n;
}
sslots[id].p = *s;
sslots[id].s = sslots[id].sp = VK_NULL_HANDLE;
sslots[id].used = 1;
samp_gen++;
unlock();
}
// DLSS upscaling (vk_sampler_dlss_bias): the scene's material textures, the filtered mipmapped ones that repeat, are
// sampled for the output's resolution; clamped ones (post effects, LUTs, the UI) keep the game's bias.
static float dlss_bias;
static int dlss_biased(const samp_par *k) {
int rep_s = k->ws == NVN_WRAP_MODE_REPEAT || k->ws == NVN_WRAP_MODE_MIRRORED_REPEAT;
int rep_t = k->wt == NVN_WRAP_MODE_REPEAT || k->wt == NVN_WRAP_MODE_MIRRORED_REPEAT;
return !k->cmp_mode && k->minf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR && (rep_s || rep_t);
}
void vk_sampler_dlss_bias(float b) {
lock();
if (b != dlss_bias) {
dlss_bias = b;
for (int i = 0; i < nsslots; i++) sslots[i].s = sslots[i].sp = VK_NULL_HANDLE; // looked up again (the key moves)
samp_gen++;
}
unlock();
}
// Caller holds the lock.
static VkSampler sampler_for(VkCtx *c, const samp_par *s) {
samp_par k;
memset(&k, 0, sizeof k);
k = *s;
if (dlss_bias != 0 && dlss_biased(&k)) k.lod_bias += dlss_bias; // part of the key: samplers live until shutdown
VkSampler v = (VkSampler)nvn_hm_get(&samplers, &k);
if (!v) {
v = make_sampler(c, &k);
nvn_hm_put(&samplers, &k, (void *)v);
}
return v;
}
VkSampler vk_sampler_for(const samp_par *s) {
VkCtx *c = vk_ctx();
if (!c || !s) return VK_NULL_HANDLE;
lock();
VkSampler v = sampler_for(c, s);
unlock();
return v;
}
// Samplers live until shutdown, so a slot keeps the one its parameters map to until it is registered again.
VkSampler vk_sampler_by_id_locked(int id) {
VkCtx *c = vk_ctx();
if (!c) return VK_NULL_HANDLE;
VkSampler v = VK_NULL_HANDLE;
if (id >= 0 && id < nsslots && sslots[id].used) {
SampSlot *s = &sslots[id];
if (!s->s) s->s = sampler_for(c, &s->p);
v = s->s;
}
return v;
}
VkSampler vk_sampler_point_by_id_locked(int id) {
VkCtx *c = vk_ctx();
if (!c || id < 0 || id >= nsslots || !sslots[id].used) return VK_NULL_HANDLE;
SampSlot *s = &sslots[id];
if (!s->sp) {
samp_par k = s->p;
k.magf = NVN_MAG_FILTER_NEAREST;
k.minf = k.minf == NVN_MIN_FILTER_LINEAR ? NVN_MIN_FILTER_NEAREST :
k.minf > NVN_MIN_FILTER_LINEAR ? NVN_MIN_FILTER_NEAREST_MIPMAP_NEAREST : k.minf;
k.aniso = 1;
s->sp = sampler_for(c, &k);
}
return s->sp;
}
VkSampler vk_sampler_by_id(int id) {
lock();
VkSampler v = vk_sampler_by_id_locked(id);
unlock();
return v;
}
int vk_sampler_params(int id, samp_par *out) {
lock();
int ok = id >= 0 && id < nsslots && sslots[id].used;
if (ok) *out = sslots[id].p;
unlock();
return ok;
}
// ---------------------------------------------------------------- statistics and shutdown
void vk_texture_stats(uint64_t *images, uint64_t *uploads, uint64_t *reuploads) {
lock();
if (images) *images = by_key.n;
if (uploads) *uploads = n_uploads;
if (reuploads) *reuploads = n_reuploads;
unlock();
}
void vk_texture_census(VkTexCensus *s) {
VkCtx *c = vk_ctx();
memset(s, 0, sizeof *s);
lock();
for (uint32_t i = 0; i < by_key.cap; i++) {
const Tex *e = by_key.vals[i];
if (!e) continue;
uint64_t bytes = 0;
if (e->alloc && c) {
VmaAllocationInfo ai;
vmaGetAllocationInfo(c->vma, e->alloc, &ai);
bytes = ai.size;
}
if (e->released) s->released++, s->own_released += e->alloc != NULL, s->bytes_released += bytes;
else s->live++, s->own_live += e->alloc != NULL, s->bytes_live += bytes;
}
unlock();
}
// TOTK_TRACE=perf, with mem: vram: what the released entries in own memory hold and how long they waited.
void vk_texture_released_log(uint64_t frame) {
VkCtx *c = vk_ctx();
if (!c) return;
enum { CL_CLEAN, CL_BACKED, CL_DIRTY, CL_POOLS, CL_N };
uint64_t cl_n[CL_N] = {0}, cl_b[CL_N] = {0}, age_b[AGE_N] = {0}, dead_n = 0, dead_b = 0, dead_dirty_b = 0;
uint64_t bound_n = 0, bound_b = 0, ded_n = 0, ded_b = 0;
typedef struct { uint32_t w, h, fmt, n; uint64_t b; } Group;
Group gr[256];
int ng = 0;
lock();
AddrIndex x = {0};
int indexed = addr_index(&x);
for (int i = 0; indexed && i < x.n; i++) {
Tex *e = x.e[i];
if (!e->released || !e->alloc) continue;
VmaAllocationInfo2 ai2;
vmaGetAllocationInfo2(c->vma, e->alloc, &ai2);
const VmaAllocationInfo ai = ai2.allocationInfo;
if (ai2.dedicatedMemory) ded_n++, ded_b += ai.size;
int cl = !e->pub.mirrored ? CL_POOLS : e->gpu_dirty ? CL_DIRTY : e->pub.gpu_owned ? CL_BACKED : CL_CLEAN;
cl_n[cl]++, cl_b[cl] += ai.size;
if (cl != CL_CLEAN) age_b[age_bucket(coarse_ms - e->rel_ms)] += ai.size;
if (e->checked && submit_epoch - e->checked < 600) bound_n++, bound_b += ai.size;
if (storage_taken(&x, i)) dead_n++, dead_b += ai.size, dead_dirty_b += cl != CL_CLEAN ? ai.size : 0;
int g = 0;
while (g < ng && (gr[g].w != e->t.d.width || gr[g].h != e->t.d.height || gr[g].fmt != (uint32_t)e->t.d.format)) g++;
if (g == ng && ng < 256) gr[ng++] = (Group){e->t.d.width, e->t.d.height, (uint32_t)e->t.d.format, 0, 0};
if (g < ng) gr[g].n++, gr[g].b += ai.size;
}
if (indexed) addr_index_free(&x);
const double M = 1048576.0;
rt_log("mem: released frame %llu: own memory: clean %llu %.0f MiB, GPU-written with storage current %llu %.0f MiB, "
"GPU-written image only %llu %.0f MiB, not mirrored %llu %.0f MiB; all but clean by age <1s %.0f <5s %.0f <30s %.0f "
"<120s %.0f older %.0f MiB; storage taken by a later texture %llu %.0f MiB (%.0f not clean); bound through a slot "
"in the last 600 submits %llu %.0f MiB; in dedicated memory, returned to the driver when dropped, %llu %.0f MiB; "
"longest search %.2f ms",
(unsigned long long)frame, (unsigned long long)cl_n[CL_CLEAN], cl_b[CL_CLEAN] / M, (unsigned long long)cl_n[CL_BACKED],
cl_b[CL_BACKED] / M, (unsigned long long)cl_n[CL_DIRTY], cl_b[CL_DIRTY] / M, (unsigned long long)cl_n[CL_POOLS],
cl_b[CL_POOLS] / M, age_b[AGE_1S] / M, age_b[AGE_5S] / M, age_b[AGE_30S] / M, age_b[AGE_120S] / M,
age_b[AGE_LONG] / M, (unsigned long long)dead_n, dead_b / M, dead_dirty_b / M, (unsigned long long)bound_n,
bound_b / M, (unsigned long long)ded_n, ded_b / M, pressure_search_ns / 1e6);
pressure_search_ns = 0;
for (int k = 0; k < 2; k++) {
char line[256];
size_t o = 0;
for (int a = 0; a < AGE_N; a++) {
o += (size_t)snprintf(line + o, sizeof line - o, " %s %llu", age_name[a], (unsigned long long)takeback_n[k][a]);
takeback_n[k][a] = 0;
}
rt_log("mem: released frame %llu: taken back after,%s:%s", (unsigned long long)frame, k ? " GPU-written" : " others", line);
}
rt_log("mem: released frame %llu: dropped under pressure and created again: within 10 s %llu (%llu GPU-written), later %llu (%llu)",
(unsigned long long)frame, (unsigned long long)n_recreated[0], (unsigned long long)n_recreated_written[0],
(unsigned long long)n_recreated[1], (unsigned long long)n_recreated_written[1]);
n_recreated[0] = n_recreated[1] = n_recreated_written[0] = n_recreated_written[1] = 0;
for (int top = 0; top < 4; top++) {
int best = -1;
for (int g = 0; g < ng; g++)
if (gr[g].n && (best < 0 || gr[g].b > gr[best].b)) best = g;
if (best < 0) break;
rt_log("mem: released frame %llu: largest %d: %ux%u fmt %#x: %u entries %.0f MiB", (unsigned long long)frame, top + 1,
gr[best].w, gr[best].h, gr[best].fmt, gr[best].n, gr[best].b / M);
gr[best].n = 0;
}
unlock();
}
void vk_texture_shutdown(void) {
VkCtx *c = vk_ctx();
lock();
Tex **list = malloc(sizeof *list * (size_t)(by_key.n + 1));
int n = 0;
for (uint32_t i = 0; i < by_key.cap; i++)
if (by_key.vals[i]) list[n++] = by_key.vals[i];
for (int i = 0; i < n; i++) destroy_tex(list[i]);
free(list);
for (uint32_t i = 0; i < samplers.cap; i++)
if (samplers.vals[i] && c) vkDestroySampler(c->device, (VkSampler)samplers.vals[i], NULL);
free(by_key.keys), free(by_key.vals), free(by_addr.keys), free(by_addr.vals), free(samplers.keys), free(samplers.vals);
by_key = (nvn_hmap){.klen = sizeof(TexKey)};
by_addr = (nvn_hmap){.klen = 8};
samplers = (nvn_hmap){.klen = sizeof(samp_par)};
free(tslots), free(sslots);
tslots = NULL, sslots = NULL, ntslots = nsslots = 0, nreleased = 0, rel_bytes_total = 0;
rel_head = rel_tail = NULL;
if (sinfo) memset(sinfo, 0, sizeof *sinfo << 20), memset(sepoch, 0, sizeof *sepoch << 20); // no slot is registered: tokens 0
unlock();
}