Files
Xenia-Canary/src/xenia/gpu/texture_util.cc

386 lines
14 KiB
C++

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2018 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/gpu/texture_util.h"
#include <algorithm>
#include "xenia/base/assert.h"
#include "xenia/base/math.h"
namespace xe {
namespace gpu {
namespace texture_util {
void GetSubresourcesFromFetchConstant(
const xenos::xe_gpu_texture_fetch_t& fetch, uint32_t* width_out,
uint32_t* height_out, uint32_t* depth_or_faces_out, uint32_t* base_page_out,
uint32_t* mip_page_out, uint32_t* mip_min_level_out,
uint32_t* mip_max_level_out, TextureFilter sampler_mip_filter) {
uint32_t width = 0, height = 0, depth_or_faces = 0;
switch (fetch.dimension) {
case Dimension::k1D:
assert_false(fetch.stacked);
assert_false(fetch.tiled);
assert_false(fetch.packed_mips);
width = fetch.size_1d.width;
break;
case Dimension::k2D:
width = fetch.size_2d.width;
height = fetch.size_2d.height;
depth_or_faces = fetch.stacked ? fetch.size_2d.stack_depth : 0;
break;
case Dimension::k3D:
assert_false(fetch.stacked);
width = fetch.size_3d.width;
height = fetch.size_3d.height;
depth_or_faces = fetch.size_3d.depth;
break;
case Dimension::kCube:
assert_false(fetch.stacked);
assert_true(fetch.size_2d.stack_depth == 5);
width = fetch.size_2d.width;
height = fetch.size_2d.height;
depth_or_faces = 5;
break;
}
++width;
++height;
++depth_or_faces;
if (width_out) {
*width_out = width;
}
if (height_out) {
*height_out = height;
}
if (depth_or_faces_out) {
*depth_or_faces_out = depth_or_faces;
}
uint32_t size_mip_max_level = GetSmallestMipLevel(
width, height, fetch.dimension == Dimension::k3D ? depth_or_faces : 1,
false);
TextureFilter mip_filter = sampler_mip_filter == TextureFilter::kUseFetchConst
? fetch.mip_filter
: sampler_mip_filter;
uint32_t base_page = fetch.base_address & 0x1FFFF;
uint32_t mip_page = fetch.mip_address & 0x1FFFF;
uint32_t mip_min_level, mip_max_level;
if (mip_filter == TextureFilter::kBaseMap || mip_page == 0) {
mip_min_level = 0;
mip_max_level = 0;
} else {
mip_min_level = std::min(fetch.mip_min_level, size_mip_max_level);
mip_max_level = std::max(std::min(fetch.mip_max_level, size_mip_max_level),
mip_min_level);
}
if (mip_max_level != 0) {
// Special case for streaming. Games such as Banjo-Kazooie: Nuts & Bolts
// specify the same address for both the base level and the mips and set
// mip_min_index to 1 until the texture is actually loaded - this is the way
// recommended by a GPU hang error message found in game executables. In
// this case we assume that the base level is not loaded yet.
if (base_page == mip_page) {
base_page = 0;
}
if (base_page == 0) {
mip_min_level = std::max(mip_min_level, uint32_t(1));
}
if (mip_min_level != 0) {
base_page = 0;
}
} else {
mip_page = 0;
}
if (base_page_out) {
*base_page_out = base_page;
}
if (mip_page_out) {
*mip_page_out = mip_page;
}
if (mip_min_level_out) {
*mip_min_level_out = mip_min_level;
}
if (mip_max_level_out) {
*mip_max_level_out = mip_max_level;
}
}
void GetGuestMipBlocks(Dimension dimension, uint32_t width, uint32_t height,
uint32_t depth, TextureFormat format, uint32_t mip,
uint32_t& width_blocks_out, uint32_t& height_blocks_out,
uint32_t& depth_blocks_out) {
// Get mipmap size.
if (mip != 0) {
width = std::max(xe::next_pow2(width) >> mip, 1u);
if (dimension != Dimension::k1D) {
height = std::max(xe::next_pow2(height) >> mip, 1u);
if (dimension == Dimension::k3D) {
depth = std::max(xe::next_pow2(depth) >> mip, 1u);
}
}
}
// Get the size in blocks rather than in pixels.
const FormatInfo* format_info = FormatInfo::Get(format);
width = xe::align(width, format_info->block_width) / format_info->block_width;
height =
xe::align(height, format_info->block_height) / format_info->block_height;
// 32x32x4-align.
width_blocks_out = xe::align(width, 32u);
if (dimension != Dimension::k1D) {
height_blocks_out = xe::align(height, 32u);
if (dimension == Dimension::k3D) {
depth_blocks_out = xe::align(depth, 4u);
} else {
depth_blocks_out = 1;
}
} else {
height_blocks_out = 1;
}
}
uint32_t GetGuestMipSliceStorageSize(uint32_t width_blocks,
uint32_t height_blocks,
uint32_t depth_blocks, bool is_tiled,
TextureFormat format,
uint32_t* row_pitch_out, bool align_4kb) {
const FormatInfo* format_info = FormatInfo::Get(format);
uint32_t row_pitch = width_blocks * format_info->block_width *
format_info->block_height * format_info->bits_per_pixel /
8;
if (!is_tiled) {
row_pitch = xe::align(row_pitch, 256u);
}
if (row_pitch_out != nullptr) {
*row_pitch_out = row_pitch;
}
uint32_t size = row_pitch * height_blocks * depth_blocks;
if (align_4kb) {
size = xe::align(size, 4096u);
}
return size;
}
bool GetPackedMipOffset(uint32_t width, uint32_t height, uint32_t depth,
TextureFormat format, uint32_t mip, uint32_t& x_blocks,
uint32_t& y_blocks, uint32_t& z_blocks) {
// Tile size is 32x32, and once textures go <=16 they are packed into a
// single tile together. The math here is insane. Most sourced from
// graph paper, looking at dds dumps and executable reverse engineering.
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// 0 +.4x4.+ +.....8x8.....+ +............16x16............+
// 1 +.4x4.+ +.....8x8.....+ +............16x16............+
// 2 +.4x4.+ +.....8x8.....+ +............16x16............+
// 3 +.4x4.+ +.....8x8.....+ +............16x16............+
// 4 x +.....8x8.....+ +............16x16............+
// 5 +.....8x8.....+ +............16x16............+
// 6 +.....8x8.....+ +............16x16............+
// 7 +.....8x8.....+ +............16x16............+
// 8 2x2 +............16x16............+
// 9 2x2 +............16x16............+
// 0 +............16x16............+
// ... .....
//
// The 2x2 and 1x1 squares are packed in their specific positions because
// each square is the size of at least one block (which is 4x4 pixels max)
//
// if (tile_aligned(w) > tile_aligned(h)) {
// // wider than tall, so packed horizontally
// } else if (tile_aligned(w) < tile_aligned(h)) {
// // taller than wide, so packed vertically
// } else {
// square
// }
// It's important to use logical sizes here, as the input sizes will be
// for the entire packed tile set, not the actual texture.
// The minimum dimension is what matters most: if either width or height
// is <= 16 this mode kicks in.
uint32_t log2_width = xe::log2_ceil(width);
uint32_t log2_height = xe::log2_ceil(height);
uint32_t log2_size = std::min(log2_width, log2_height);
if (log2_size > 4 + mip) {
// The shortest dimension is bigger than 16, not packed.
x_blocks = 0;
y_blocks = 0;
z_blocks = 0;
return false;
}
uint32_t packed_mip_base = (log2_size > 4) ? (log2_size - 4) : 0;
uint32_t packed_mip = mip - packed_mip_base;
// Find the block offset of the mip.
if (packed_mip < 3) {
if (log2_width > log2_height) {
// Wider than tall. Laid out vertically.
x_blocks = 0;
y_blocks = 16 >> packed_mip;
} else {
// Taller than wide. Laid out horizontally.
x_blocks = 16 >> packed_mip;
y_blocks = 0;
}
z_blocks = 0;
} else {
uint32_t offset;
if (log2_width > log2_height) {
// Wider than tall. Laid out horizontally.
offset = (1 << (log2_width - packed_mip_base)) >> (packed_mip - 2);
x_blocks = offset;
y_blocks = 0;
} else {
// Taller than wide. Laid out vertically.
x_blocks = 0;
offset = (1 << (log2_height - packed_mip_base)) >> (packed_mip - 2);
y_blocks = offset;
}
if (offset < 4) {
// Pack 1x1 Z mipmaps along Z - not reached for 2D.
uint32_t log2_depth = xe::log2_ceil(depth);
if (log2_depth > 1 + packed_mip) {
z_blocks = (log2_depth - packed_mip) * 4;
} else {
z_blocks = 4;
}
} else {
z_blocks = 0;
}
}
const FormatInfo* format_info = FormatInfo::Get(format);
x_blocks /= format_info->block_width;
y_blocks /= format_info->block_height;
return true;
}
void GetTextureTotalSize(Dimension dimension, uint32_t width, uint32_t height,
uint32_t depth, TextureFormat format, bool is_tiled,
bool packed_mips, uint32_t mip_max_level,
uint32_t* base_size_out, uint32_t* mip_size_out) {
bool is_3d = dimension == Dimension::k3D;
uint32_t width_blocks, height_blocks, depth_blocks;
if (base_size_out) {
GetGuestMipBlocks(dimension, width, height, depth, format, 0, width_blocks,
height_blocks, depth_blocks);
uint32_t size = GetGuestMipSliceStorageSize(
width_blocks, height_blocks, depth_blocks, is_tiled, format, nullptr);
if (!is_3d) {
size *= depth;
}
*base_size_out = size;
}
if (mip_size_out) {
mip_max_level = std::min(
mip_max_level,
GetSmallestMipLevel(width, height, is_3d ? depth : 1, packed_mips));
uint32_t size = 0;
for (uint32_t i = 1; i <= mip_max_level; ++i) {
GetGuestMipBlocks(dimension, width, height, depth, format, i,
width_blocks, height_blocks, depth_blocks);
uint32_t level_size = GetGuestMipSliceStorageSize(
width_blocks, height_blocks, depth_blocks, is_tiled, format, nullptr);
if (!is_3d) {
level_size *= depth;
}
size += level_size;
}
*mip_size_out = size;
}
}
int32_t GetTiledOffset2D(int32_t x, int32_t y, uint32_t width,
uint32_t bpb_log2) {
// https://github.com/gildor2/UModel/blob/de8fbd3bc922427ea056b7340202dcdcc19ccff5/Unreal/UnTexture.cpp#L489
width = xe::align(width, 32u);
// Top bits of coordinates.
int32_t macro = ((x >> 5) + (y >> 5) * (width >> 5)) << (bpb_log2 + 7);
// Lower bits of coordinates (result is 6-bit value).
int32_t micro = ((x & 7) + ((y & 0xE) << 2)) << bpb_log2;
// Mix micro/macro + add few remaining x/y bits.
int32_t offset =
macro + ((micro & ~0xF) << 1) + (micro & 0xF) + ((y & 1) << 4);
// Mix bits again.
return ((offset & ~0x1FF) << 3) + ((y & 16) << 7) + ((offset & 0x1C0) << 2) +
(((((y & 8) >> 2) + (x >> 3)) & 3) << 6) + (offset & 0x3F);
}
int32_t GetTiledOffset3D(int32_t x, int32_t y, int32_t z, uint32_t width,
uint32_t height, uint32_t bpb_log2) {
// Reconstructed from disassembly of XGRAPHICS::TileVolume.
width = xe::align(width, 32u);
height = xe::align(height, 32u);
int32_t macro_outer = ((y >> 4) + (z >> 2) * (height >> 4)) * (width >> 5);
int32_t macro = ((((x >> 5) + macro_outer) << (bpb_log2 + 6)) & 0xFFFFFFF)
<< 1;
int32_t micro = (((x & 7) + ((y & 6) << 2)) << (bpb_log2 + 6)) >> 6;
int32_t offset_outer = ((y >> 3) + (z >> 2)) & 1;
int32_t offset1 =
offset_outer + ((((x >> 3) + (offset_outer << 1)) & 3) << 1);
int32_t offset2 = ((macro + (micro & ~15)) << 1) + (micro & 15) +
((z & 3) << (bpb_log2 + 6)) + ((y & 1) << 4);
int32_t address = (offset1 & 1) << 3;
address += (offset2 >> 6) & 7;
address <<= 3;
address += offset1 & ~1;
address <<= 2;
address += offset2 & ~511;
address <<= 3;
address += offset2 & 63;
return address;
}
uint8_t SwizzleSigns(const xenos::xe_gpu_texture_fetch_t& fetch) {
uint8_t signs = 0;
bool any_not_signed = false, any_signed = false;
// 0b00 or 0b01 for each component, whether it's constant 0/1.
uint8_t constant_mask = 0b00000000;
for (uint32_t i = 0; i < 4; ++i) {
uint32_t swizzle = (fetch.swizzle >> (i * 3)) & 0b111;
if (swizzle & 0b100) {
constant_mask |= uint8_t(1) << (i * 2);
} else {
TextureSign sign =
TextureSign((fetch.dword_0 >> (2 + swizzle * 2)) & 0b11);
signs |= uint8_t(sign) << (i * 2);
if (sign == TextureSign::kSigned) {
any_signed = true;
} else {
any_not_signed = true;
}
}
}
TextureSign constants_sign = TextureSign::kUnsigned;
if (constant_mask == 0b01010101) {
// If only constant components, choose according to the original format
// (what would more likely be loaded if there were non-constant components).
// If all components would be signed, use signed.
if (((fetch.dword_0 >> 2) & 0b11111111) ==
uint32_t(TextureSign::kSigned) * 0b01010101) {
constants_sign = TextureSign::kSigned;
}
} else {
// If only signed and constant components, reading just from the signed host
// view is enough.
if (any_signed && !any_not_signed) {
constants_sign = TextureSign::kSigned;
}
}
signs |= uint8_t(constants_sign) * constant_mask;
return signs;
}
} // namespace texture_util
} // namespace gpu
} // namespace xe