#define PVR_REGS_FOR_RENDERER // see hw/pvr/pvr_regs.h
/*
* Created on: Oct 3, 2019
Copyright 2019 flyinghead
This file is part of Flycast.
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see .
*/
#include
#include "texture.h"
#include "utils.h"
#include
#include
void setImageLayout(vk::CommandBuffer const& commandBuffer, vk::Image image, vk::Format format, u32 mipmapLevels, vk::ImageLayout oldImageLayout, vk::ImageLayout newImageLayout)
{
vk::AccessFlags sourceAccessMask;
switch (oldImageLayout)
{
case vk::ImageLayout::eTransferDstOptimal:
sourceAccessMask = vk::AccessFlagBits::eTransferWrite;
break;
case vk::ImageLayout::eTransferSrcOptimal:
sourceAccessMask = vk::AccessFlagBits::eTransferRead;
break;
case vk::ImageLayout::ePreinitialized:
sourceAccessMask = vk::AccessFlagBits::eHostWrite;
break;
case vk::ImageLayout::eGeneral: // sourceAccessMask is empty
case vk::ImageLayout::eUndefined:
break;
case vk::ImageLayout::eShaderReadOnlyOptimal:
sourceAccessMask = vk::AccessFlagBits::eShaderRead;
break;
default:
verify(false);
break;
}
vk::PipelineStageFlags sourceStage;
switch (oldImageLayout)
{
case vk::ImageLayout::eGeneral:
case vk::ImageLayout::ePreinitialized:
sourceStage = vk::PipelineStageFlagBits::eHost;
break;
case vk::ImageLayout::eTransferDstOptimal:
case vk::ImageLayout::eTransferSrcOptimal:
sourceStage = vk::PipelineStageFlagBits::eTransfer;
break;
case vk::ImageLayout::eUndefined:
sourceStage = vk::PipelineStageFlagBits::eTopOfPipe;
break;
case vk::ImageLayout::eShaderReadOnlyOptimal:
sourceStage = vk::PipelineStageFlagBits::eFragmentShader;
break;
default:
verify(false);
break;
}
vk::AccessFlags destinationAccessMask;
switch (newImageLayout)
{
case vk::ImageLayout::eColorAttachmentOptimal:
destinationAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
break;
case vk::ImageLayout::eDepthStencilAttachmentOptimal:
destinationAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;
break;
case vk::ImageLayout::eGeneral: // empty destinationAccessMask
break;
case vk::ImageLayout::eShaderReadOnlyOptimal:
destinationAccessMask = vk::AccessFlagBits::eShaderRead;
break;
case vk::ImageLayout::eTransferSrcOptimal:
destinationAccessMask = vk::AccessFlagBits::eTransferRead;
break;
case vk::ImageLayout::eTransferDstOptimal:
destinationAccessMask = vk::AccessFlagBits::eTransferWrite;
break;
case vk::ImageLayout::eDepthStencilReadOnlyOptimal:
destinationAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead;
break;
default:
verify(false);
break;
}
vk::PipelineStageFlags destinationStage;
switch (newImageLayout)
{
case vk::ImageLayout::eColorAttachmentOptimal:
destinationStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
break;
case vk::ImageLayout::eDepthStencilAttachmentOptimal:
destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests;
break;
case vk::ImageLayout::eGeneral:
destinationStage = vk::PipelineStageFlagBits::eHost;
break;
case vk::ImageLayout::eShaderReadOnlyOptimal:
destinationStage = vk::PipelineStageFlagBits::eFragmentShader;
break;
case vk::ImageLayout::eTransferDstOptimal:
case vk::ImageLayout::eTransferSrcOptimal:
destinationStage = vk::PipelineStageFlagBits::eTransfer;
break;
case vk::ImageLayout::eDepthStencilReadOnlyOptimal:
destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests;
break;
default:
verify(false);
break;
}
vk::ImageAspectFlags aspectMask;
if (newImageLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal || newImageLayout == vk::ImageLayout::eDepthStencilReadOnlyOptimal)
{
aspectMask = vk::ImageAspectFlagBits::eDepth;
if (format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint || format == vk::Format::eD16UnormS8Uint)
{
aspectMask |= vk::ImageAspectFlagBits::eStencil;
}
}
else
{
aspectMask = vk::ImageAspectFlagBits::eColor;
}
vk::ImageSubresourceRange imageSubresourceRange(aspectMask, 0, mipmapLevels, 0, 1);
vk::ImageMemoryBarrier imageMemoryBarrier(sourceAccessMask, destinationAccessMask, oldImageLayout, newImageLayout, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, image, imageSubresourceRange);
commandBuffer.pipelineBarrier(sourceStage, destinationStage, {}, nullptr, nullptr, imageMemoryBarrier);
}
#include "rend/upload_layout.h"
#include "tex_decoder.h"
#include "commandpool.h"
#include "hw/pvr/pvr_mem.h"
TexDecoder *Texture::decoder;
TexturePool *Texture::pool;
u64 Texture::viewSerials;
Texture::~Texture()
{
CancelDecode();
/* (a cache's texture goes when nothing has drawn with it for long, or
* when the renderer does: its image is free at once) */
ReleasePooled(true);
}
/* ---- The texture pool ---- */
/* The formats the pool has images of, by its numbers for them: the
* converters' five, the two the decoder copies planar textures into, and
* the pictures rendered to */
static const vk::Format poolFormats[] = {
vk::Format::eR5G5B5A1UnormPack16, vk::Format::eR5G6B5UnormPack16, vk::Format::eR4G4B4A4UnormPack16,
vk::Format::eR8G8B8A8Unorm, vk::Format::eR8Unorm,
vk::Format::eA1R5G5B5UnormPack16, vk::Format::eB4G4R4A4UnormPack16,
vk::Format::eR8G8B8A8Unorm,
};
static const char * const poolFormatNames[] = { "5551", "565", "4444", "8888", "8-bit", "1555", "4444 (planar)", "8888 rendered to" };
// what the pictures rendered to are used for (the drawers' renderTargetUsage, and to be written as a texture after)
static const vk::ImageUsageFlags baseTargetUsage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled
| vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst;
int TexturePool::FormatIndex(vk::Format format)
{
for (int i = 0; i < FORMATS; i++)
if (poolFormats[i] == format)
return i;
return -1;
}
void TexturePool::Init(const TexDecoder *decoder)
{
if (ready)
return;
VulkanContext *context = VulkanContext::Instance();
const vk::PhysicalDevice physicalDevice = context->GetPhysicalDevice();
this->decoder = decoder;
book.Reset(TexPoolBook::MAX_FRAMES);
static const u32 texel[FORMATS] = { 2, 2, 2, 4, 1, 2, 2, 4 };
// how much games use them, in eighths of what they use 565
static const u32 use[FORMATS] = { 6, 8, 6, 4, 2, 1, 1, 0 };
static const u8 kind[FORMATS] = { 0, 0, 0, 0, 0, 0, 0, TexPoolBook::TARGETS };
u32 have[FORMATS];
for (int f = 0; f < FORMATS; f++)
{
formats[f] = poolFormats[f];
const vk::FormatProperties props = physicalDevice.getFormatProperties(formats[f]);
const vk::FormatFeatureFlags needed = vk::FormatFeatureFlagBits::eSampledImage | vk::FormatFeatureFlagBits::eTransferDst
| vk::FormatFeatureFlagBits::eTransferSrc;
usable[f] = (props.optimalTilingFeatures & needed) == needed;
if (f == TARGET)
{
usable[f] = usable[f] && (props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eColorAttachment);
// (the compute renderer writes them as storage images: SetTargetStorage())
if (!(props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eStorageImage))
targetStorage = false;
targetUsage = baseTargetUsage | (targetStorage ? vk::ImageUsageFlagBits::eStorage : vk::ImageUsageFlags());
}
// the 16-bit ones the device cannot sample are converted to 32 bits (Force32BitTexture())
if (f < 3 && !context->IsFormatSupported((TextureType)(f == 0 ? TextureType::_5551 : f == 1 ? TextureType::_565 : TextureType::_4444)))
usable[f] = false;
if (f >= 5 && f != TARGET)
usable[f] = usable[f] && decoder != nullptr && decoder->NativeFormat(f == 5 ? TextureType::_5551 : TextureType::_4444) == formats[f];
storable[f] = f < 5 && f != TARGET && decoder != nullptr && decoder->Ready() && decoder->Takes(formats[f]);
have[f] = usable[f] ? texel[f] : 0;
}
u32 counts[TexPoolBook::CLASSES];
const u64 bytes = TexPoolBook::Plan(context->GetVramBudget(), have, use, kind, FORMATS, counts,
texcache_upscale_factor(), (u32)settings.rend.MaxFilteredTextureSize,
settings.rend.RenderToTextureUpscale > 1 ? (u32)settings.rend.RenderToTextureUpscale : 1u);
ready = true;
for (int c = 0; c < TexPoolBook::CLASSES; c++)
for (u32 i = 0; i < counts[c]; i++)
{
PooledImage *image = Make(c, false);
if (image == nullptr)
break;
book.Give(image);
}
NOTICE_LOG(RENDERER, "Vulkan: texture pool of %u images, %u MB (planned %u MB)", book.made, (u32)(book.bytesMade >> 20), (u32)(bytes >> 20));
}
void TexturePool::Term()
{
if (!ready)
return;
book.Summary("Vulkan");
while (all != nullptr)
{
PooledImage *next = all->all;
delete all;
all = next;
}
book.Reset(TexPoolBook::MAX_FRAMES);
ready = false;
decoder = nullptr;
}
PooledImage *TexturePool::Make(int cls, bool late)
{
const vk::Device device = VulkanContext::Instance()->GetDevice();
const u32 f = TexPoolBook::Format(cls);
const u32 width = TexPoolBook::Width(cls), height = TexPoolBook::Height(cls);
const vk::Format format = formats[f];
PooledImage *pi = new PooledImage();
pi->format = format;
pi->levels = TexPoolBook::Mips(cls) ? 1 + bitscanrev(std::max(width, height)) : 1;
pi->storage = storable[f];
pi->usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eTransferSrc;
if (f == TARGET)
pi->usage = targetUsage;
vk::ImageCreateFlags flags;
if (pi->storage)
{
/* written through views of a format of integers of the same size:
* the 16- and 8-bit formats cannot be storage images themselves */
pi->usage |= vk::ImageUsageFlagBits::eStorage;
flags = vk::ImageCreateFlagBits::eMutableFormat;
if (format != vk::Format::eR8G8B8A8Unorm)
flags |= vk::ImageCreateFlagBits::eExtendedUsage;
}
pi->image = device.createImageUnique(vk::ImageCreateInfo(flags, vk::ImageType::e2D, format, vk::Extent3D(width, height, 1), pi->levels, 1,
vk::SampleCountFlagBits::e1, vk::ImageTiling::eOptimal, pi->usage, vk::SharingMode::eExclusive, 0, nullptr, vk::ImageLayout::eUndefined));
pi->allocation = VulkanContext::Instance()->GetHeap().AllocateForImage(*pi->image, false, false);
/* A 4444 texture copied as it is: the PowerVR has alpha, red, green
* and blue from the top, which this format calls blue, green, red and
* alpha */
const vk::ComponentMapping components = format == vk::Format::eB4G4R4A4UnormPack16
? vk::ComponentMapping(vk::ComponentSwizzle::eG, vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eA, vk::ComponentSwizzle::eB)
: vk::ComponentMapping();
vk::ImageViewCreateInfo viewInfo(vk::ImageViewCreateFlags(), *pi->image, vk::ImageViewType::e2D, format, components,
vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, pi->levels, 0, 1));
// (sampled only: the image's other uses are through views of other formats)
const vk::ImageViewUsageCreateInfo viewUsage(pi->usage & ~vk::ImageUsageFlagBits::eStorage);
if (flags & vk::ImageCreateFlagBits::eExtendedUsage)
viewInfo.pNext = &viewUsage;
pi->view = device.createImageViewUnique(viewInfo);
pi->viewSerial = Texture::NewViewSerial();
if (pi->storage)
{
const vk::Format viewFormat = format == vk::Format::eR8G8B8A8Unorm ? vk::Format::eR32Uint
: format == vk::Format::eR8Unorm ? vk::Format::eR8Uint : vk::Format::eR16Uint;
const vk::ImageViewUsageCreateInfo storageUsage(vk::ImageUsageFlagBits::eStorage);
for (u32 level = 0; level < pi->levels; level++)
{
vk::ImageViewCreateInfo info(vk::ImageViewCreateFlags(), *pi->image, vk::ImageViewType::e2D, viewFormat, vk::ComponentMapping(),
vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, level, 1, 0, 1));
info.pNext = &storageUsage;
pi->storageViews[level] = device.createImageViewUnique(info);
}
}
book.Made(pi, cls, TexPoolBook::Bytes(cls, f == 3 || f == TARGET ? 4 : f == 4 ? 1 : 2), late, poolFormatNames[f]);
pi->all = all;
all = pi;
return pi;
}
PooledImage *TexturePool::Take(vk::Format format, u32 width, u32 height, bool mips)
{
const int f = FormatIndex(format);
if (!ready || f < 0 || !usable[f])
return nullptr;
const int cls = TexPoolBook::Class((u32)f, width, height, mips);
if (cls < 0)
return nullptr;
TexPoolEntry *e = book.Take(cls);
return e != nullptr ? static_cast(e) : Make(cls, true);
}
PooledImage *TexturePool::TakeTarget(u32 width, u32 height, const vk::ImageUsageFlags& usage)
{
if (!ready || !usable[TARGET] || (usage & targetUsage) != usage)
return nullptr;
const int cls = TexPoolBook::Class(TARGET, width, height, false);
if (cls < 0)
return nullptr;
TexPoolEntry *e = book.Take(cls);
return e != nullptr ? static_cast(e) : Make(cls, true);
}
u32 Texture::outsideCleared;
void Texture::ClearOutside(vk::CommandBuffer cmd, u32 width, u32 height, vk::ImageLayout& layout)
{
const u32 area = width << 16 | height;
const bool whole = width >= extent.width && height >= extent.height;
if (!whole && (area != rttArea || layout == vk::ImageLayout::eUndefined))
{
setImageLayout(cmd, GetImage(), format, 1, layout, vk::ImageLayout::eTransferDstOptimal);
cmd.clearColorImage(GetImage(), vk::ImageLayout::eTransferDstOptimal,
vk::ClearColorValue(std::array { 0.f, 0.f, 0.f, 1.f }),
vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1));
layout = vk::ImageLayout::eTransferDstOptimal;
outsideCleared++;
}
rttArea = area;
}
void Texture::CreateRenderTarget(const vk::ImageUsageFlags& usage)
{
PooledImage *pi = pool != nullptr ? pool->TakeTarget(extent.width, extent.height, usage) : nullptr;
if (pi == nullptr)
{
CreateImage(vk::ImageTiling::eOptimal, usage, vk::ImageLayout::eUndefined, vk::ImageAspectFlagBits::eColor);
return;
}
// what it had goes, as CreateImage() lets it go
CancelDecode();
ReleasePooled(false);
for (auto& view : storageViews)
view.reset();
imageView.reset();
image.reset();
allocation = Allocation();
pooled = pi;
// (a texture of the cache's, whose image is the pool's to take back)
cached = true;
mipmapLevels = 1;
imageUsage = pi->usage;
storage = false;
fresh = true;
debugPath = DBG_TARGET;
debugLevels = 0;
}
bool Texture::TakePooled(u32 width, u32 height, bool mipmapped)
{
if (pool == nullptr || !cached)
return false;
PooledImage *pi = pool->Take(format, width, height, mipmapped);
if (pi == nullptr)
return false;
// what it had goes, as CreateImage() lets it go
CancelDecode();
ReleasePooled(false);
for (auto& view : storageViews)
view.reset();
imageView.reset();
image.reset();
allocation = Allocation();
pooled = pi;
mipmapLevels = pi->levels;
imageUsage = pi->usage;
storage = pi->storage;
needsStaging = true;
// nothing in it is kept: whatever was drawn into it is another texture's
fresh = true;
return true;
}
void Texture::ReleasePooled(bool now)
{
if (pooled == nullptr)
return;
CancelDecode();
if (pool != nullptr)
{
if (now)
pool->Give(pooled);
else
pool->Retire(pooled);
}
pooled = nullptr;
storage = false;
}
/* A texel of this texture's, in bytes */
static u32 texel_bytes(TextureType type)
{
return type == TextureType::_8888 ? 4 : type == TextureType::_8 ? 1 : 2;
}
UploadRing& Texture::Ring()
{
static UploadRing ring;
return ring;
}
void UploadRing::Init(size_t bytes, CommandPool *pool)
{
this->pool = pool;
if (mapped != nullptr && size == (u64)bytes)
return;
Term();
this->pool = pool;
buffer = std::unique_ptr(new BufferData(bytes, vk::BufferUsageFlagBits::eTransferSrc));
mapped = (u8 *)buffer->MapMemory();
size = bytes;
head = tail = 0;
current = 0;
for (u32 i = 0; i < MAX_FRAMES; i++)
frameLive[i] = false;
INFO_LOG(RENDERER, "Vulkan: upload ring of %u KB", (u32)(bytes >> 10));
}
void UploadRing::Term()
{
if (mapped != nullptr)
NOTICE_LOG(RENDERER, "Vulkan: upload ring: %u MB handed out, %u frames found done before their turn, %u textures put off for want of room",
(u32)(head >> 20), (u32)statPolled, (u32)statFull);
mapped = nullptr;
buffer.reset();
size = head = tail = 0;
for (u32 i = 0; i < MAX_FRAMES; i++)
frameLive[i] = false;
}
void UploadRing::SetCurrentIndex(int index)
{
if (mapped == nullptr)
return;
// where the frame that ends here ends; then this place's last frame is done
frameEnd[current % MAX_FRAMES] = head;
frameLive[current % MAX_FRAMES] = true;
current = (u32)index;
if (frameLive[current % MAX_FRAMES])
{
tail = std::max(tail, frameEnd[current % MAX_FRAMES]);
frameLive[current % MAX_FRAMES] = false;
}
}
u8 *UploadRing::Allocate(size_t bytes, vk::Buffer *buffer, vk::DeviceSize *offset)
{
if (mapped == nullptr)
return nullptr;
bytes = (bytes + 15) & ~(size_t)15;
if (bytes > size)
{
statFull++;
return nullptr;
}
for (;;)
{
const u64 at = head % size;
// (a piece is never split: from the start again if it would be)
const u64 skip = at + bytes > size ? size - at : 0;
if (head + skip + bytes - tail <= size)
{
head += skip;
u8 *memory = mapped + head % size;
*buffer = this->buffer->buffer.get();
*offset = head % size;
head += bytes;
return memory;
}
/* Full: the oldest frame still out, if its fence says it is done
* (the frames are done in the order they went) */
const int frames = pool != nullptr ? std::min(pool->GetSize(), (int)MAX_FRAMES) : 0;
bool freed = false;
for (int k = 1; k < frames && !freed; k++)
{
const u32 slot = (current + (u32)k) % (u32)frames;
if (!frameLive[slot])
continue;
if (!pool->IsDone((int)slot))
break;
tail = std::max(tail, frameEnd[slot]);
frameLive[slot] = false;
statPolled++;
freed = true;
}
if (!freed)
{
statFull++;
return nullptr;
}
}
}
/* Makes the image what a texture of this size and kind needs, if it is not
* already, for @maker to write; in format @native, if one is given.
* Returns whether it was made anew. */
bool Texture::Prepare(int width, int height, bool mipmapped, bool mipmapsIncluded, Maker maker, vk::Format native)
{
rttArea = 0; // (written other than by a render: ClearOutside())
vk::Format format = vk::Format::eUndefined;
switch (tex_type)
{
case TextureType::_5551:
format = vk::Format::eR5G5B5A1UnormPack16;
break;
case TextureType::_565:
format = vk::Format::eR5G6B5UnormPack16;
break;
case TextureType::_4444:
format = vk::Format::eR4G4B4A4UnormPack16;
break;
case TextureType::_8888:
format = vk::Format::eR8G8B8A8Unorm;
break;
case TextureType::_8:
format = vk::Format::eR8Unorm;
break;
}
if (native != vk::Format::eUndefined)
format = native;
if (width == (int)extent.width && height == (int)extent.height && format == this->format
// an image made for the CPU to write where it is, or not for the kernel
&& (maker == CPU || needsStaging) && (maker != KERNEL || storage)
&& (imageUsage & vk::ImageUsageFlagBits::eTransferDst || !needsStaging))
return false;
Init(width, height, format, width * height * texel_bytes(tex_type), mipmapped, mipmapsIncluded, maker);
return true;
}
void Texture::CancelDecode()
{
if (decodeJob >= 0 && decoder != nullptr && decodeSerial == decoder->Serial() && decoder->Cancel(decodeJob))
// what was to make it from nothing did not: the next upload does
fresh = true;
decodeJob = -1;
}
/* The kernel's, or a copy of the texels as they are: see tex_decoder.h.
* Everything that can say no does before anything is changed. */
bool Texture::GpuDecode(const texd_plan& plan)
{
if (decoder == nullptr || !decoder->Ready())
return false;
const vk::Format nativeFormat = plan.native ? decoder->NativeFormat(tex_type) : vk::Format::eUndefined;
const bool copy = nativeFormat != vk::Format::eUndefined;
if (!copy)
{
vk::Format format;
switch (tex_type)
{
case TextureType::_5551: format = vk::Format::eR5G5B5A1UnormPack16; break;
case TextureType::_565: format = vk::Format::eR5G6B5UnormPack16; break;
case TextureType::_4444: format = vk::Format::eR4G4B4A4UnormPack16; break;
case TextureType::_8888: format = vk::Format::eR8G8B8A8Unorm; break;
default: format = vk::Format::eR8Unorm; break;
}
if (!decoder->Takes(format))
return false;
}
const u32 bytes = plan.pal + plan.pal_entries * 4;
if (!decoder->Room(bytes, copy ? 0 : plan.levels))
return false;
CancelDecode();
prepared = false;
Prepare(plan.width, plan.height, IsMipmapped(), plan.mipmapped != 0, copy ? COPY : KERNEL, nativeFormat);
u32 offset;
u8 * const to = decoder->Reserve(bytes, &offset);
memcpy(to, &vram[plan.src], plan.bytes);
if (plan.pal_entries != 0)
memcpy(to + plan.pal, (plan.out == TEXD_OUT16 ? palette16_ram : palette32_ram) + plan.pal_index, plan.pal_entries * 4);
if (copy)
decodeJob = decoder->AddCopy(GetImage(), fresh, offset, plan.level[0].sr & 0xFFFF, plan.width, plan.height);
else
{
TexDecoder::Level levels[TEXD_MAX_LEVELS];
for (u32 i = 0; i < plan.levels; i++)
{
const texd_level& lv = plan.level[i];
levels[i].view = StorageView(lv.mip);
levels[i].pc[0] = offset / 4;
levels[i].pc[1] = lv.data;
levels[i].pc[2] = plan.pal;
levels[i].pc[3] = lv.wh;
levels[i].pc[4] = lv.sr;
levels[i].pc[5] = lv.mode;
levels[i].group[0] = 0;
levels[i].group[1] = 0;
levels[i].group[2] = ((lv.wh & 0xFFFF) + 7) / 8;
levels[i].group[3] = ((lv.wh >> 16) + 7) / 8;
}
decodeJob = decoder->AddDecode(GetImage(), fresh, mipmapLevels, plan.out, levels, plan.levels);
}
decodeSerial = decoder->Serial();
fresh = false;
debugPath = copy ? DBG_COPY : DBG_KERNEL;
debugLevels = 0;
return true;
}
/* A copy, or a blit where the render is finer, from the render's image -
* one that can be copied from, sampled where it is - in the commands the
* texture's uploads go in, which the GPU runs after the render's. */
bool Texture::CopyRender(BaseTextureCacheData *from, u32 x, u32 y, u32 scale, u32 columns)
{
rttArea = 0; // (written other than by a render: ClearOutside())
Texture *src = static_cast(from);
const vk::Format rgba = vk::Format::eR8G8B8A8Unorm;
if (commandPool == nullptr || src->IsNew() || src->format != rgba || !(src->imageUsage & vk::ImageUsageFlagBits::eTransferSrc)
|| (x + columns) * scale > src->extent.width || (y + h) * scale > src->extent.height)
return false;
CancelDecode();
prepared = false;
tex_type = TextureType::_8888;
const bool isNew = Prepare(w, h, false, false, COPY, rgba) || fresh;
if (!(imageUsage & vk::ImageUsageFlagBits::eTransferDst))
return false;
if (!commandBuffer)
commandBuffer = commandPool->Allocate();
commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
setImageLayout(commandBuffer, src->GetImage(), rgba, 1, vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eTransferSrcOptimal);
setImageLayout(commandBuffer, GetImage(), rgba, mipmapLevels, isNew ? vk::ImageLayout::eUndefined : vk::ImageLayout::eShaderReadOnlyOptimal,
vk::ImageLayout::eTransferDstOptimal);
const vk::ImageSubresourceLayers layers(vk::ImageAspectFlagBits::eColor, 0, 0, 1);
if (columns < w)
{
// (the rest of each row, as a texture read through a narrower stride has it)
const vk::ImageSubresourceRange range(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1);
commandBuffer.clearColorImage(GetImage(), vk::ImageLayout::eTransferDstOptimal,
vk::ClearColorValue(std::array { 0.f, 0.f, 0.f, ClearOpaque() ? 1.f : 0.f }), range);
const vk::MemoryBarrier barrier(vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eTransferWrite);
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, barrier, nullptr, nullptr);
}
if (scale <= 1)
commandBuffer.copyImage(src->GetImage(), vk::ImageLayout::eTransferSrcOptimal, GetImage(), vk::ImageLayout::eTransferDstOptimal,
vk::ImageCopy(layers, vk::Offset3D(x, y, 0), layers, vk::Offset3D(0, 0, 0), vk::Extent3D(columns, h, 1)));
else
{
const std::array from_box = { vk::Offset3D(x * scale, y * scale, 0), vk::Offset3D((x + columns) * scale, (y + h) * scale, 1) };
const std::array to_box = { vk::Offset3D(0, 0, 0), vk::Offset3D(columns, h, 1) };
commandBuffer.blitImage(src->GetImage(), vk::ImageLayout::eTransferSrcOptimal, GetImage(), vk::ImageLayout::eTransferDstOptimal,
vk::ImageBlit(layers, from_box, layers, to_box), vk::Filter::eLinear);
}
setImageLayout(commandBuffer, GetImage(), rgba, mipmapLevels, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);
setImageLayout(commandBuffer, src->GetImage(), rgba, 1, vk::ImageLayout::eTransferSrcOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);
commandBuffer.end();
fresh = false;
debugPath = DBG_COPY;
debugLevels = 0;
return true;
}
/* The image's format for texels of @type */
static vk::Format type_format(TextureType type)
{
switch (type)
{
case TextureType::_5551: return vk::Format::eR5G5B5A1UnormPack16;
case TextureType::_565: return vk::Format::eR5G6B5UnormPack16;
case TextureType::_4444: return vk::Format::eR4G4B4A4UnormPack16;
case TextureType::_8888: return vk::Format::eR8G8B8A8Unorm;
default: return vk::Format::eR8Unorm;
}
}
/* The image the last whole update made, as it made it (in @format), and
* nothing for it still to be done this frame */
bool Texture::PartReady(vk::Format format) const
{
const u32 levels = made_mips ? 1 + bitscanrev(w) : 1;
if (made_type != (u8)tex_type + 1 || IsNew() || fresh || this->format != format
|| extent.width != w || extent.height != h || mipmapLevels != levels || debugPath == DBG_TARGET)
return false;
return decodeJob < 0 || decoder == nullptr || decodeSerial != decoder->Serial();
}
bool Texture::PartMemory(size_t bytes, void **memory)
{
if (!needsStaging || !(imageUsage & vk::ImageUsageFlagBits::eTransferDst) || !PartReady(type_format(tex_type)))
return false;
partSlot = Ring().Allocate(bytes, &partBuffer, &partOffset);
*memory = partSlot;
return partSlot != nullptr;
}
void Texture::UploadPart(const TexRegion *regions, u32 count, u8 *data, const size_t *offset)
{
rttArea = 0; // (written other than by a render: ClearOutside())
vk::BufferImageCopy copies[TEXP_REGIONS];
u32 i;
if (!commandBuffer && commandPool != nullptr)
commandBuffer = commandPool->Allocate();
verify((bool)commandBuffer);
for (i = 0; i < count; i++)
copies[i] = vk::BufferImageCopy(partOffset + offset[i], regions[i].w, regions[i].h,
vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, regions[i].level, 0, 1),
vk::Offset3D((int32_t)regions[i].x, (int32_t)regions[i].y, 0), vk::Extent3D(regions[i].w, regions[i].h, 1));
/* (what the image has is kept: from what it is sampled as) */
commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
setImageLayout(commandBuffer, GetImage(), format, mipmapLevels, vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eTransferDstOptimal);
commandBuffer.copyBufferToImage(partBuffer, GetImage(), vk::ImageLayout::eTransferDstOptimal, count, copies);
setImageLayout(commandBuffer, GetImage(), format, mipmapLevels, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);
commandBuffer.end();
partSlot = nullptr;
debugPath = DBG_STAGED;
debugLevels = 0;
}
int Texture::GpuDecodePart(const texd_plan& plan, const TexRegion *regions, u32 count)
{
rttArea = 0; // (written other than by a render: ClearOutside())
TexDecoder::Level levels[TEXP_REGIONS];
TexPartPlan part;
vk::Format nativeFormat, format;
bool copy;
u8 *to;
u32 offset, i;
if (decoder == nullptr || !decoder->Ready())
return TEXP_CPU;
nativeFormat = plan.native ? decoder->NativeFormat(tex_type) : vk::Format::eUndefined;
copy = nativeFormat != vk::Format::eUndefined;
format = copy ? nativeFormat : type_format(tex_type);
if (!copy && !decoder->Takes(format))
return TEXP_CPU;
/* an image the whole update made otherwise is made in part as that was */
if (!PartReady(format))
return PartReady(type_format(tex_type)) ? TEXP_CPU : TEXP_WHOLE;
if (copy ? !(imageUsage & vk::ImageUsageFlagBits::eTransferDst) : !StorageView(0) || !decoder->RunsPart())
return TEXP_CPU;
if (copy)
{
/* the rows, as they are: from the first region's to the last's, in one copy */
const u32 stride = plan.level[0].sr & 0xFFFF;
u32 y0 = plan.height, y1 = 0, bytes;
for (i = 0; i < count; i++)
{
y0 = std::min(y0, regions[i].y);
y1 = std::max(y1, regions[i].y + regions[i].h);
}
bytes = (y1 - y0) * stride * 2;
if (y0 >= y1 || !decoder->Room(bytes, 0))
return TEXP_CPU;
to = decoder->Reserve(bytes, &offset);
memcpy(to, &vram[plan.src + plan.level[0].data + y0 * stride * 2], bytes);
decodeJob = decoder->AddCopy(GetImage(), false, offset, stride, plan.width, y1 - y0, y0);
}
else
{
if (!tex_plan_part(&plan, regions, count, &part) || !decoder->Room(part.total, count))
return TEXP_CPU;
to = decoder->Reserve(part.total, &offset);
memcpy(to, &vram[plan.src], part.codebook);
for (i = 0; i < count; i++)
memcpy(to + part.at[i], &vram[plan.src + part.from[i]], part.bytes[i]);
if (plan.pal_entries != 0)
memcpy(to + part.pal, (plan.out == TEXD_OUT16 ? palette16_ram : palette32_ram) + plan.pal_index, plan.pal_entries * 4);
for (i = 0; i < count; i++)
{
const texd_level& lv = plan.level[part.level[i]];
levels[i].view = StorageView(lv.mip);
levels[i].pc[0] = offset / 4;
levels[i].pc[1] = part.data[i];
levels[i].pc[2] = part.pal;
levels[i].pc[3] = lv.wh;
levels[i].pc[4] = lv.sr;
levels[i].pc[5] = lv.mode;
levels[i].group[0] = regions[i].x / 8;
levels[i].group[1] = regions[i].y / 8;
levels[i].group[2] = (regions[i].w + 7) / 8;
levels[i].group[3] = (regions[i].h + 7) / 8;
}
decodeJob = decoder->AddDecode(GetImage(), false, mipmapLevels, plan.out, levels, count);
}
decodeSerial = decoder->Serial();
debugPath = copy ? DBG_COPY : DBG_KERNEL;
debugLevels = 0;
return TEXP_DONE;
}
void *Texture::UploadMemory(u32 width, u32 height, size_t bytes, u32 pixel, bool mipmaps)
{
CancelDecode();
prepared = true;
preparedNew = Prepare(width, height, IsMipmapped(), mipmaps);
if (!needsStaging)
// a small image the texture is written into where it is, at the pitch it has
return nullptr;
const u32 pad = mipmaps && mipmapLevels > 1 ? upload_levels_pad(pixel) : 0;
slot = Ring().Allocate(pad + bytes, &slotBuffer, &slotOffset);
// (no room: converted into the cache's memory, and SetImage() finds none either)
return slot != nullptr ? slot + pad : nullptr;
}
void Texture::UploadToGPU(int width, int height, u8 *data, bool mipmapped, bool mipmapsIncluded)
{
const u32 pixel = texel_bytes(tex_type);
// the texels as they are handed over: the levels after one another, or the one image
u32 dataSize = width * height * pixel;
if (mipmapsIncluded)
for (int w = width / 2; w != 0; w /= 2)
dataSize += w * w * pixel;
if (!prepared)
{
CancelDecode();
preparedNew = Prepare(width, height, mipmapped, mipmapsIncluded);
}
prepared = false;
SetImage(dataSize, data, preparedNew, mipmapped && !mipmapsIncluded);
slot = nullptr;
}
void Texture::Init(u32 width, u32 height, vk::Format format, u32 dataSize, bool mipmapped, bool mipmapsIncluded, Maker maker)
{
this->extent = vk::Extent2D(width, height);
this->format = format;
mipmapLevels = 1;
if (mipmapped)
mipmapLevels += floor(log2(std::max(width, height)));
// an image of the pool's, made before the game: optimal, written through a staging copy or by the GPU
if (TakePooled(width, height, mipmapped))
return;
if (maker != CPU)
{
/* Optimal, written by the GPU, and copied to as well: a texture
* made here once can be converted on the CPU the next time */
needsStaging = true;
vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eTransferSrc;
vk::ImageCreateFlags flags;
if (maker == KERNEL)
{
/* written through views of a format of integers of the same
* size: the 16- and 8-bit formats cannot be storage images
* themselves */
usage |= vk::ImageUsageFlagBits::eStorage;
flags = vk::ImageCreateFlagBits::eMutableFormat;
if (format != vk::Format::eR8G8B8A8Unorm)
flags |= vk::ImageCreateFlagBits::eExtendedUsage;
}
CreateImage(vk::ImageTiling::eOptimal, usage, vk::ImageLayout::eUndefined, vk::ImageAspectFlagBits::eColor, flags);
if (maker == KERNEL)
{
const vk::Format viewFormat = format == vk::Format::eR8G8B8A8Unorm ? vk::Format::eR32Uint
: format == vk::Format::eR8Unorm ? vk::Format::eR8Uint : vk::Format::eR16Uint;
const vk::ImageViewUsageCreateInfo viewUsage(vk::ImageUsageFlagBits::eStorage);
for (u32 level = 0; level < mipmapLevels; level++)
{
vk::ImageViewCreateInfo viewInfo(vk::ImageViewCreateFlags(), image.get(), vk::ImageViewType::e2D, viewFormat, vk::ComponentMapping(),
vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, level, 1, 0, 1));
viewInfo.pNext = &viewUsage;
storageViews[level] = device.createImageViewUnique(viewInfo);
}
storage = true;
}
return;
}
vk::FormatProperties formatProperties = physicalDevice.getFormatProperties(format);
vk::ImageTiling imageTiling = (formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage)
== vk::FormatFeatureFlagBits::eSampledImage
? vk::ImageTiling::eOptimal
: vk::ImageTiling::eLinear;
if (height <= 32
&& dataSize / height <= 64
&& !mipmapped
&& (formatProperties.linearTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage) == vk::FormatFeatureFlagBits::eSampledImage)
imageTiling = vk::ImageTiling::eLinear;
needsStaging = imageTiling != vk::ImageTiling::eLinear;
vk::ImageLayout initialLayout;
vk::ImageUsageFlags usageFlags = vk::ImageUsageFlagBits::eSampled;
if (needsStaging)
{
usageFlags |= vk::ImageUsageFlagBits::eTransferDst;
initialLayout = vk::ImageLayout::eUndefined;
}
else
{
verify((formatProperties.linearTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage) == vk::FormatFeatureFlagBits::eSampledImage);
initialLayout = vk::ImageLayout::ePreinitialized;
}
if (mipmapped && !mipmapsIncluded)
usageFlags |= vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst;
if (presented)
usageFlags |= vk::ImageUsageFlagBits::eTransferSrc;
CreateImage(imageTiling, usageFlags, initialLayout, vk::ImageAspectFlagBits::eColor);
}
void Texture::CreateImage(vk::ImageTiling tiling, const vk::ImageUsageFlags& usage, vk::ImageLayout initialLayout,
const vk::ImageAspectFlags& aspectMask, vk::ImageCreateFlags flags)
{
// the old image's storage views go with it
CancelDecode();
ReleasePooled(false);
for (auto& view : storageViews)
view.reset();
storage = false;
fresh = true;
imageUsage = usage;
vk::ImageCreateInfo imageCreateInfo(flags, vk::ImageType::e2D, format, vk::Extent3D(extent, 1), mipmapLevels, 1,
vk::SampleCountFlagBits::e1, tiling, usage,
vk::SharingMode::eExclusive, 0, nullptr, initialLayout);
image = device.createImageUnique(imageCreateInfo);
allocation = VulkanContext::Instance()->GetHeap().AllocateForImage(*image,
tiling == vk::ImageTiling::eLinear, !needsStaging);
/* A 4444 texture copied as it is: the PowerVR has alpha, red, green
* and blue from the top, which this format calls blue, green, red and
* alpha */
const vk::ComponentMapping components = format == vk::Format::eB4G4R4A4UnormPack16
? vk::ComponentMapping(vk::ComponentSwizzle::eG, vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eA, vk::ComponentSwizzle::eB)
: vk::ComponentMapping();
vk::ImageViewCreateInfo imageViewCreateInfo(vk::ImageViewCreateFlags(), image.get(), vk::ImageViewType::e2D, format, components,
vk::ImageSubresourceRange(aspectMask, 0, mipmapLevels, 0, 1));
// (sampled only: the image's other uses are through views of other formats)
const vk::ImageViewUsageCreateInfo viewUsage(usage & ~vk::ImageUsageFlagBits::eStorage);
if (flags & vk::ImageCreateFlagBits::eExtendedUsage)
imageViewCreateInfo.pNext = &viewUsage;
imageView = device.createImageViewUnique(imageViewCreateInfo);
ownSerial = NewViewSerial();
}
void Texture::Defer(bool isNew)
{
if (isNew)
{
/* (an image never written is sampled all the same: it is made one
* that can be, whatever is in it) */
commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
setImageLayout(commandBuffer, GetImage(), format, mipmapLevels, vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal);
commandBuffer.end();
}
fresh = false;
made_type = 0;
if (cached)
dirty = FrameCount != 0 ? FrameCount : 1;
}
/* The texture debug log's checksum of @size bytes (FNV-1a) */
static u32 DebugSum(const u8 *p, u32 size)
{
u32 h = 2166136261u;
for (u32 i = 0; i < size; i++)
h = (h ^ p[i]) * 16777619u;
return h;
}
void Texture::SetImage(u32 srcSize, void *srcData, bool isNew, bool genMipmaps)
{
rttArea = 0; // (written other than by a render: ClearOutside())
if (!commandBuffer && commandPool != nullptr)
commandBuffer = commandPool->Allocate();
verify((bool)commandBuffer);
isNew = isNew || fresh;
debugLevels = 0;
if (needsStaging)
{
const u32 pixel = texel_bytes(tex_type);
const u32 pad = mipmapLevels > 1 && !genMipmaps ? upload_levels_pad(pixel) : 0;
/* Converted into its piece of the ring already - or, for what was
* not (a custom texture, one scaled up, the renderer's own small
* textures, one there was no room for), a piece for it now, and the
* one copy. None, and it waits for the next frame. */
if (slot == nullptr || srcData != slot + pad)
{
slot = Ring().Allocate(pad + srcSize, &slotBuffer, &slotOffset);
if (slot == nullptr)
{
Defer(isNew);
return;
}
memcpy(slot + pad, srcData, srcSize);
}
}
fresh = false;
commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
if (!isNew && !needsStaging)
setImageLayout(commandBuffer, GetImage(), format, mipmapLevels, vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eGeneral);
if (needsStaging)
{
const u32 pixel = texel_bytes(tex_type);
const bool levels = mipmapLevels > 1 && !genMipmaps;
const u32 pad = levels ? upload_levels_pad(pixel) : 0;
if (pad != 0)
// the 1x1 level, to where a copy can start: see upload_layout.h
memcpy(slot, slot + pad, pixel);
// Since we're going to blit to the texture image, set its layout to eTransferDstOptimal
setImageLayout(commandBuffer, GetImage(), format, mipmapLevels, isNew ? vk::ImageLayout::eUndefined : vk::ImageLayout::eShaderReadOnlyOptimal,
vk::ImageLayout::eTransferDstOptimal);
if (levels)
{
for (u32 i = 0; i < mipmapLevels; i++)
{
vk::BufferImageCopy copyRegion(slotOffset + upload_level_at(pixel, i), 1 << i, 1 << i,
vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, mipmapLevels - i - 1, 0, 1),
vk::Offset3D(0, 0, 0), vk::Extent3D(1 << i, 1 << i, 1));
commandBuffer.copyBufferToImage(slotBuffer, GetImage(), vk::ImageLayout::eTransferDstOptimal, copyRegion);
if (settings.rend.TextureDebug)
debugSum[mipmapLevels - i - 1] = DebugSum(slot + upload_level_at(pixel, i), (1u << i) * (1u << i) * pixel);
}
debugLevels = settings.rend.TextureDebug ? mipmapLevels : 0;
}
else
{
if (settings.rend.TextureDebug)
{
debugSum[0] = DebugSum(slot, extent.width * extent.height * pixel);
debugLevels = 1;
}
vk::BufferImageCopy copyRegion(slotOffset, extent.width, extent.height, vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1),
vk::Offset3D(0, 0, 0), vk::Extent3D(extent, 1));
commandBuffer.copyBufferToImage(slotBuffer, GetImage(), vk::ImageLayout::eTransferDstOptimal, copyRegion);
}
if (mipmapLevels > 1 && !levels)
// (every level left for the shaders to read)
GenerateMipmaps();
else
// Set the layout for the texture image from eTransferDstOptimal to SHADER_READ_ONLY
setImageLayout(commandBuffer, GetImage(), format, mipmapLevels, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);
debugPath = DBG_STAGED;
}
else
{
void *data = allocation.MapMemory();
verify(data != nullptr);
vk::SubresourceLayout layout = device.getImageSubresourceLayout(GetImage(), vk::ImageSubresource(vk::ImageAspectFlagBits::eColor, 0, 0));
if (layout.size != srcSize)
{
u8 *src = (u8 *)srcData;
u8 *dst = (u8 *)data;
u32 srcSz = extent.width * 2;
if (tex_type == TextureType::_8888)
srcSz *= 2;
else if (tex_type == TextureType::_8)
srcSz /= 2;
u8 * const srcEnd = src + srcSz * extent.height;
for (; src < srcEnd; src += srcSz, dst += layout.rowPitch)
memcpy(dst, src, srcSz);
}
else
memcpy(data, srcData, srcSize);
debugPath = DBG_LINEAR;
debugLevels = 0;
if (mipmapLevels > 1)
GenerateMipmaps();
else
// If we can use the linear tiled image as a texture, just do it
setImageLayout(commandBuffer, GetImage(), format, mipmapLevels, isNew ? vk::ImageLayout::ePreinitialized : vk::ImageLayout::eGeneral,
vk::ImageLayout::eShaderReadOnlyOptimal);
}
commandBuffer.end();
}
void Texture::GenerateMipmaps()
{
u32 mipWidth = extent.width;
u32 mipHeight = extent.height;
vk::ImageMemoryBarrier barrier(vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eTransferRead,
vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eTransferSrcOptimal, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED,
GetImage(), vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1));
for (u32 i = 1; i < mipmapLevels; i++)
{
// Transition previous mipmap level from dst optimal/preinit to src optimal
barrier.subresourceRange.baseMipLevel = i - 1;
if (i == 1 && !needsStaging)
{
barrier.oldLayout = vk::ImageLayout::ePreinitialized;
barrier.srcAccessMask = vk::AccessFlagBits::eHostWrite;
}
else
{
barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
}
barrier.newLayout = vk::ImageLayout::eTransferSrcOptimal;
barrier.dstAccessMask = vk::AccessFlagBits::eTransferRead;
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, nullptr, nullptr, barrier);
// Blit previous mipmap level on current
vk::ImageBlit blit(vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, i - 1, 0, 1),
{ { vk::Offset3D(0, 0, 0), vk::Offset3D(mipWidth, mipHeight, 1) } },
vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, i, 0, 1),
{ { vk::Offset3D(0, 0, 0), vk::Offset3D(std::max(mipWidth / 2, 1u), std::max(mipHeight / 2, 1u), 1) } });
commandBuffer.blitImage(GetImage(), vk::ImageLayout::eTransferSrcOptimal, GetImage(), vk::ImageLayout::eTransferDstOptimal, 1, &blit, vk::Filter::eLinear);
// Transition previous mipmap level from src optimal to shader read-only optimal
barrier.oldLayout = vk::ImageLayout::eTransferSrcOptimal;
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
barrier.srcAccessMask = vk::AccessFlagBits::eTransferRead;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, nullptr, nullptr, barrier);
mipWidth = std::max(mipWidth / 2, 1u);
mipHeight = std::max(mipHeight / 2, 1u);
}
// Transition last mipmap level from dst optimal to shader read-only optimal
barrier.subresourceRange.baseMipLevel = mipmapLevels - 1;
barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, nullptr, nullptr, barrier);
}
void FramebufferAttachment::Init(u32 width, u32 height, vk::Format format, const vk::ImageUsageFlags& usage, bool readable,
vk::SampleCountFlagBits samples)
{
this->format = format;
this->extent = vk::Extent2D { width, height };
bool depth = format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint || format == vk::Format::eD16UnormS8Uint;
if (readable && (usage & vk::ImageUsageFlagBits::eTransferSrc))
{
stagingBufferData = std::unique_ptr(new BufferData(width * height * 4,
vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst));
}
vk::ImageCreateInfo imageCreateInfo(vk::ImageCreateFlags(), vk::ImageType::e2D, format, vk::Extent3D(extent, 1), 1, 1, samples,
vk::ImageTiling::eOptimal, usage,
vk::SharingMode::eExclusive, 0, nullptr, vk::ImageLayout::eUndefined);
image = device.createImageUnique(imageCreateInfo);
allocation = VulkanContext::Instance()->GetHeap().AllocateForImage(*image, false, false,
(bool)(usage & vk::ImageUsageFlagBits::eTransientAttachment));
vk::ImageViewCreateInfo imageViewCreateInfo(vk::ImageViewCreateFlags(), image.get(), vk::ImageViewType::e2D,
format, vk::ComponentMapping(), vk::ImageSubresourceRange(depth ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1));
imageView = device.createImageViewUnique(imageViewCreateInfo);
viewSerial = Texture::NewViewSerial();
if ((usage & vk::ImageUsageFlagBits::eDepthStencilAttachment) && (usage & vk::ImageUsageFlagBits::eInputAttachment))
{
// Also create an imageView for the stencil
imageViewCreateInfo.subresourceRange = vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1);
stencilView = device.createImageViewUnique(imageViewCreateInfo);
}
}
void TextureCache::Cleanup()
{
/* (a few a frame, those not used for 120 frames) */
u64 list[6];
u32 n = 0;
u32 TargetFrame = std::max((u32)120, FrameCount) - 120;
cache.Each([&](u64 key, Texture& texture) {
if (texture.dirty && texture.dirty < TargetFrame)
list[n++] = key;
return n < 6;
});
for (u32 i = 0; i < n; i++)
{
if (clearTexture(cache.Find(list[i])))
cache.Remove(list[i]);
}
}
/* Bytes a texel of a texture image's @format */
static u32 DebugTexelBytes(vk::Format format)
{
switch (format)
{
case vk::Format::eR8Unorm:
return 1;
case vk::Format::eR8G8B8A8Unorm:
case vk::Format::eB8G8R8A8Unorm:
return 4;
default:
return 2;
}
}
/* The texture debug log (settings.rend.TextureDebug), for finding out on a
* machine where a texture is drawn wrong what the textures and their
* samplers are there. About every ten seconds, a line for each texture of
* the frame drawn last: what the game asked for, the image made of it, how
* it was written, the sampler it is drawn with, and each level of the image
* read back from the GPU with its checksum, beside the checksum of what the
* CPU uploaded into that level (the GPU's own decode and generated levels
* have none). The read back waits for the GPU, and the buffer for it is made
* each time: this is a diagnostic, off unless asked for. */
void TextureCache::DebugLog(Texture *const *textures, u32 count)
{
static u32 lastFrame;
if (FrameCount < 300 || (lastFrame != 0 && FrameCount - lastFrame < 600) || count == 0)
return;
lastFrame = FrameCount;
VulkanContext *context = VulkanContext::Instance();
const vk::Device device = context->GetDevice();
vk::PhysicalDeviceProperties props;
context->GetPhysicalDevice().getProperties(&props);
NOTICE_LOG(RENDERER, "TexDebug: frame %u, %u textures; %s (driver %08x); sub-pixel bits %u, sub-texel bits %u, mipmap bits %u, LOD bias max %.1f; force LOD0 %d, anisotropy %d, GPU decode %d, upscale %d",
FrameCount, count, (const char *)&props.deviceName[0], props.driverVersion, props.limits.subPixelPrecisionBits,
props.limits.subTexelPrecisionBits, props.limits.mipmapPrecisionBits, props.limits.maxSamplerLodBias,
settings.rend.ForceTextureLOD0, settings.rend.AnisotropicFiltering, settings.rend.GpuTextureDecode, settings.rend.TextureUpscale);
// what is read back, and where
struct Read { Texture *t; u32 level; u32 offset; u32 bytes; };
std::vector reads;
vk::DeviceSize total = 0;
for (u32 k = 0; k < count; k++)
{
Texture *t = textures[k];
if (t == nullptr || t->format == vk::Format::eUndefined || t->fresh || t->decodeJob >= 0 || t->IsNew()
|| !(t->imageUsage & vk::ImageUsageFlagBits::eTransferSrc) || t->debugPath == Texture::DBG_TARGET)
continue;
const u32 texel = DebugTexelBytes(t->format);
for (u32 l = 0; l < t->mipmapLevels; l++)
{
const u32 w = std::max(t->extent.width >> l, 1u), h = std::max(t->extent.height >> l, 1u);
reads.push_back({ t, l, (u32)total, w * h * texel });
total = (total + w * h * texel + 3) & ~(vk::DeviceSize)3;
}
}
std::unique_ptr buffer;
if (total != 0)
{
buffer.reset(new BufferData(total, vk::BufferUsageFlagBits::eTransferDst,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent));
vk::UniqueCommandPool pool = device.createCommandPoolUnique(vk::CommandPoolCreateInfo(vk::CommandPoolCreateFlagBits::eTransient,
context->GetGraphicsQueueFamilyIndex()));
vk::CommandBuffer cmd = device.allocateCommandBuffers(vk::CommandBufferAllocateInfo(*pool, vk::CommandBufferLevel::ePrimary, 1)).front();
cmd.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
Texture *last = nullptr;
for (const Read& r : reads)
{
Texture *t = r.t;
const vk::ImageSubresourceRange all(vk::ImageAspectFlagBits::eColor, 0, t->mipmapLevels, 0, 1);
if (t != last)
{
// (after everything sent before, which wrote it or drew with it)
const vk::ImageMemoryBarrier toSrc(vk::AccessFlagBits::eMemoryWrite, vk::AccessFlagBits::eTransferRead,
vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eTransferSrcOptimal, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED,
t->GetImage(), all);
cmd.pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eTransfer, {}, nullptr, nullptr, toSrc);
}
const u32 w = std::max(t->extent.width >> r.level, 1u), h = std::max(t->extent.height >> r.level, 1u);
cmd.copyImageToBuffer(t->GetImage(), vk::ImageLayout::eTransferSrcOptimal, *buffer->buffer,
vk::BufferImageCopy(r.offset, 0, 0, vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, r.level, 0, 1),
vk::Offset3D(0, 0, 0), vk::Extent3D(w, h, 1)));
if (r.level + 1 == t->mipmapLevels)
{
const vk::ImageMemoryBarrier back(vk::AccessFlagBits::eTransferRead, vk::AccessFlagBits::eShaderRead,
vk::ImageLayout::eTransferSrcOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED,
t->GetImage(), all);
cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eAllCommands, {}, nullptr, nullptr, back);
}
last = t;
}
const vk::MemoryBarrier toHost(vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eHostRead);
cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eHost, {}, toHost, nullptr, nullptr);
cmd.end();
vk::UniqueFence fence = device.createFenceUnique(vk::FenceCreateInfo());
context->SubmitCommandBuffers(1, &cmd, *fence);
if (device.waitForFences(1, &fence.get(), true, UINT64_MAX) != vk::Result::eSuccess)
reads.clear();
}
const u8 *mem = buffer ? (const u8 *)buffer->MapMemory() : nullptr;
static const char * const paths[] = { "none", "cpu-staged", "cpu-linear", "gpu-kernel", "gpu-copy", "render-target" };
static const char * const filters[] = { "point", "bilinear", "trilinear-a", "trilinear-b" };
for (u32 k = 0; k < count; k++)
{
Texture *t = textures[k];
if (t == nullptr)
continue;
char line[1024];
const TSP tsp = t->tsp;
const char *addr = tsp.ClampU ? "clamp" : tsp.FlipU ? "mirror" : "repeat";
const char *addv = tsp.ClampV ? "clamp" : tsp.FlipV ? "mirror" : "repeat";
int n = snprintf(line, sizeof(line), "TexDebug: %08x tcw %08x tsp %08x %ux%u %s%s%s%s image %ux%u vk-format %d levels %u %s%s path %s; sampler %s u %s v %s bias %.2f aniso %d lod 0-256 mip nearest;",
t->sa_tex, t->tcw.full, tsp.full, t->w, t->h, t->GetPixelFormatName(), t->tcw.VQ_Comp ? " vq" : "",
t->tcw.MipMapped ? " mip-bit" : "", t->tcw.ScanOrder && !t->IsPaletted() ? " planar" : "",
t->extent.width, t->extent.height, (int)t->format, t->mipmapLevels, t->pooled != nullptr ? "pooled" : "own",
t->storage ? " storage" : "", paths[t->debugPath < 6 ? t->debugPath : 0],
filters[tsp.FilterMode], addr, addv, D_Adjust_LoD_Bias[tsp.MipMapD],
tsp.FilterMode != 0 && settings.rend.AnisotropicFiltering > 1 && context->SupportsSamplerAnisotropy() ? settings.rend.AnisotropicFiltering : 0);
bool any = false;
for (const Read& r : reads)
{
if (r.t != t || n >= (int)sizeof(line) - 48)
continue;
any = true;
const u32 gpu = DebugSum(mem + r.offset, r.bytes);
if (r.level < t->debugLevels)
n += snprintf(line + n, sizeof(line) - n, " L%u %08x/%08x %s", r.level, gpu, t->debugSum[r.level],
gpu == t->debugSum[r.level] ? "ok" : "DIFF");
else
n += snprintf(line + n, sizeof(line) - n, " L%u %08x", r.level, gpu);
}
if (!any)
snprintf(line + n, sizeof(line) - n, " not read back");
NOTICE_LOG(RENDERER, "%s", line);
}
}