Add texture zoo tests

* These tests ensure that texture rendering works correctly for all different
  types of texture types, and for all formats, across different APIs, including
  across a remote-proxy connection.
This commit is contained in:
baldurk
2019-11-26 17:38:27 +00:00
parent 938056a67c
commit 279a2ec69d
24 changed files with 6289 additions and 19 deletions
+3
View File
@@ -29,6 +29,7 @@ set(VULKAN_SRC
vk/vk_spec_constants.cpp
vk/vk_spirv_13_shaders.cpp
vk/vk_structured_buffer_nested.cpp
vk/vk_texture_zoo.cpp
vk/vk_triangle_fan.cpp
vk/vk_vertex_attr_zoo.cpp
vk/vk_video_textures.cpp
@@ -58,12 +59,14 @@ set(OPENGL_SRC
gl/gl_shader_editing.cpp
gl/gl_simple_triangle.cpp
gl/gl_spirv_shader.cpp
gl/gl_texture_zoo.cpp
gl/gl_unsized_ms_fbo_attachment.cpp
gl/gl_vao_0.cpp
gl/gl_vertex_attr_zoo.cpp)
set(SRC main.cpp
test_common.cpp
texture_zoo.cpp
linux/linux_platform.cpp
linux/linux_window.cpp)
+5 -4
View File
@@ -325,15 +325,16 @@ private:
}
template <class T>
inline void SetDebugName(T pObj, const char *name)
inline void SetDebugName(T pObj, const std::string &name)
{
if(pObj)
pObj->SetPrivateData(WKPDID_D3DDebugObjectName, (UINT)strlen(name), name);
pObj->SetPrivateData(WKPDID_D3DDebugObjectName, (UINT)name.size(), name.c_str());
}
template <class T>
inline void SetDebugName(T pObj, const wchar_t *name)
inline void SetDebugName(T pObj, const std::wstring &name)
{
if(pObj)
pObj->SetPrivateData(WKPDID_D3DDebugObjectNameW, UINT(wcslen(name) * sizeof(wchar_t)), name);
pObj->SetPrivateData(WKPDID_D3DDebugObjectNameW, UINT(name.size() * sizeof(wchar_t)),
name.c_str());
}
+18
View File
@@ -334,6 +334,24 @@ void D3D11GraphicsTest::Present()
swap->Present(0, 0);
}
void D3D11GraphicsTest::pushMarker(const std::string &name)
{
if(annot)
annot->BeginEvent(UTF82Wide(name).c_str());
}
void D3D11GraphicsTest::setMarker(const std::string &name)
{
if(annot)
annot->SetMarker(UTF82Wide(name).c_str());
}
void D3D11GraphicsTest::popMarker()
{
if(annot)
annot->EndEvent();
}
std::vector<byte> D3D11GraphicsTest::GetBufferData(ID3D11Buffer *buffer, uint32_t offset, uint32_t len)
{
D3D11_MAPPED_SUBRESOURCE mapped;
+4
View File
@@ -152,6 +152,10 @@ struct D3D11GraphicsTest : public GraphicsTest
bool Running();
void Present();
void pushMarker(const std::string &name);
void setMarker(const std::string &name);
void popMarker();
DXGI_FORMAT backbufferFmt = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
int backbufferCount = 2;
int backbufferMSAA = 1;
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -700,8 +700,6 @@ D3D12ViewCreator &D3D12ViewCreator::FirstMip(UINT mip)
{
if(firstMip)
*firstMip = mip;
else
TEST_ERROR("This view & resource doesn't support FirstMip");
return *this;
}
@@ -709,8 +707,6 @@ D3D12ViewCreator &D3D12ViewCreator::NumMips(UINT num)
{
if(numMips)
*numMips = num;
else
TEST_ERROR("This view & resource doesn't support NumMips");
return *this;
}
@@ -718,8 +714,6 @@ D3D12ViewCreator &D3D12ViewCreator::FirstSlice(UINT mip)
{
if(firstSlice)
*firstSlice = mip;
else
TEST_ERROR("This view & resource doesn't support FirstSlice");
return *this;
}
@@ -727,8 +721,6 @@ D3D12ViewCreator &D3D12ViewCreator::NumSlices(UINT num)
{
if(numSlices)
*numSlices = num;
else
TEST_ERROR("This view & resource doesn't support NumSlices");
return *this;
}
@@ -742,8 +734,6 @@ D3D12ViewCreator &D3D12ViewCreator::PlaneSlice(UINT plane)
{
if(planeSlice)
*planeSlice = plane;
else
TEST_ERROR("This view & resource doesn't support NumSlices");
return *this;
}
@@ -874,6 +864,16 @@ D3D12PSOCreator::D3D12PSOCreator(D3D12GraphicsTest *test) : m_Test(test)
GraphicsDesc.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA;
GraphicsDesc.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD;
GraphicsDesc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
GraphicsDesc.DepthStencilState.DepthEnable = FALSE;
GraphicsDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
GraphicsDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL;
GraphicsDesc.DepthStencilState.StencilReadMask = 0xff;
GraphicsDesc.DepthStencilState.StencilWriteMask = 0xff;
GraphicsDesc.DepthStencilState.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
GraphicsDesc.DepthStencilState.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
GraphicsDesc.DepthStencilState.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
GraphicsDesc.DepthStencilState.FrontFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE;
GraphicsDesc.DepthStencilState.BackFace = GraphicsDesc.DepthStencilState.FrontFace;
}
D3D12PSOCreator &D3D12PSOCreator::VS(ID3DBlobPtr blob)
File diff suppressed because it is too large Load Diff
+5
View File
@@ -152,6 +152,7 @@
<ClCompile Include="d3d11\d3d11_structured_buffer_read.cpp" />
<ClCompile Include="d3d11\d3d11_test.cpp" />
<ClCompile Include="d3d11\d3d11_texture_3d.cpp" />
<ClCompile Include="d3d11\d3d11_texture_zoo.cpp" />
<ClCompile Include="d3d11\d3d11_untyped_backbuffer_descriptor.cpp" />
<ClCompile Include="d3d11\d3d11_vertex_attr_zoo.cpp" />
<ClCompile Include="d3d11\d3d11_video_textures.cpp" />
@@ -161,6 +162,7 @@
<ClCompile Include="d3d12\d3d12_resource_lifetimes.cpp" />
<ClCompile Include="d3d12\d3d12_simple_triangle.cpp" />
<ClCompile Include="d3d12\d3d12_test.cpp" />
<ClCompile Include="d3d12\d3d12_texture_zoo.cpp" />
<ClCompile Include="d3d12\d3d12_untyped_backbuffer_descriptor.cpp" />
<ClCompile Include="d3d12\d3d12_vertex_attr_zoo.cpp" />
<ClCompile Include="d3d12\d3d12_video_textures.cpp" />
@@ -200,6 +202,7 @@
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="gl\gl_test_win32.cpp" />
<ClCompile Include="gl\gl_texture_zoo.cpp" />
<ClCompile Include="gl\gl_unsized_ms_fbo_attachment.cpp" />
<ClCompile Include="gl\gl_vao_0.cpp" />
<ClCompile Include="gl\gl_vertex_attr_zoo.cpp" />
@@ -212,6 +215,7 @@
<ClCompile Include="3rdparty\lz4\lz4.c" />
<ClCompile Include="main.cpp" />
<ClCompile Include="test_common.cpp" />
<ClCompile Include="texture_zoo.cpp" />
<ClCompile Include="vk\vk_parameter_zoo.cpp" />
<ClCompile Include="vk\vk_imageless_framebuffer.cpp" />
<ClCompile Include="vk\vk_image_layouts.cpp" />
@@ -222,6 +226,7 @@
<ClCompile Include="vk\vk_shader_editing.cpp" />
<ClCompile Include="vk\vk_spec_constants.cpp" />
<ClCompile Include="vk\vk_spirv_13_shaders.cpp" />
<ClCompile Include="vk\vk_texture_zoo.cpp" />
<ClCompile Include="vk\vk_triangle_fan.cpp" />
<ClCompile Include="vk\vk_vertex_attr_zoo.cpp" />
<ClCompile Include="vk\vk_buffer_address.cpp" />
+13
View File
@@ -348,6 +348,19 @@
<ClCompile Include="d3d12\d3d12_write_subresource.cpp">
<Filter>D3D12\demos</Filter>
</ClCompile>
<ClCompile Include="d3d11\d3d11_texture_zoo.cpp">
<Filter>D3D11\demos</Filter>
</ClCompile>
<ClCompile Include="gl\gl_texture_zoo.cpp">
<Filter>OpenGL\demos</Filter>
</ClCompile>
<ClCompile Include="texture_zoo.cpp" />
<ClCompile Include="d3d12\d3d12_texture_zoo.cpp">
<Filter>D3D12\demos</Filter>
</ClCompile>
<ClCompile Include="vk\vk_texture_zoo.cpp">
<Filter>Vulkan\demos</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Filter Include="D3D11">
+985
View File
@@ -0,0 +1,985 @@
/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#include "gl_test.h"
using namespace TextureZoo;
RD_TEST(GL_Texture_Zoo, OpenGLGraphicsTest)
{
static constexpr const char *Description =
"Tests all possible combinations of texture type and format that are supported.";
std::string blitVertex = R"EOSHADER(
#version 420 core
void main()
{
const vec4 verts[4] = vec4[4](vec4(-1.0, -1.0, 0.5, 1.0), vec4(3.0, -1.0, 0.5, 1.0),
vec4(-1.0, 3.0, 0.5, 1.0), vec4(1.0, 1.0, 0.5, 1.0));
gl_Position = verts[gl_VertexID];
}
)EOSHADER";
std::string pixelTemplate = R"EOSHADER(
#version 420 core
layout(binding = 0) uniform &texdecl intex;
layout(location = 0, index = 0) out vec4 Color;
void main()
{
Color = vec4(texelFetch(intex, &params));
}
)EOSHADER";
std::string pixelMSFloat = R"EOSHADER(
#version 420 core
uniform uint texWidth;
uniform uint slice;
uniform uint mip;
uniform uint flags;
uniform uint zlayer;
float srgb2linear(float f)
{
if (f <= 0.04045f)
return f / 12.92f;
else
return pow((f + 0.055f) / 1.055f, 2.4f);
}
layout(location = 0, index = 0) out vec4 Color;
void main()
{
uint x = uint(gl_FragCoord.x);
uint y = uint(gl_FragCoord.y);
vec4 ret = vec4(0.1f, 0.35f, 0.6f, 0.85f);
// each 3D slice cycles the x. This only affects the primary diagonal
uint offs_x = (x + zlayer) % max(1u, texWidth >> mip);
// pixels off the diagonal invert the colors
if(offs_x != y)
ret = ret.wzyx;
// second slice adds a coarse checkerboard pattern of inversion
if(slice > 0 && (((x / 2) % 2) != ((y / 2) % 2)))
ret = ret.wzyx;
// second sample/mip is shifted up a bit. MSAA textures have no mips,
// textures with mips have no samples.
ret += 0.075f.xxxx * (gl_SampleID + mip);
// Signed normals are negative
if((flags & 1) != 0)
ret = -ret;
// undo SRGB curve applied in output merger, to match the textures we just blat values into
// without conversion (which are then interpreted as srgb implicitly)
if((flags & 2) != 0)
{
ret.r = srgb2linear(ret.r);
ret.g = srgb2linear(ret.g);
ret.b = srgb2linear(ret.b);
}
// BGR flip - same as above, for BGRA textures
if((flags & 4) != 0)
ret.rgb = ret.bgr;
// put red into alpha, because that's what we did in manual upload
if((flags & 8) != 0)
ret.a = ret.r;
Color = ret;
}
)EOSHADER";
std::string pixelMSDepth = R"EOSHADER(
#version 420 core
uniform uint texWidth;
uniform uint slice;
uniform uint mip;
uniform uint flags;
uniform uint zlayer;
void main()
{
uint x = uint(gl_FragCoord.x);
uint y = uint(gl_FragCoord.y);
float ret = 0.1f;
// each 3D slice cycles the x. This only affects the primary diagonal
uint offs_x = (x + zlayer) % max(1u, texWidth >> mip);
// pixels off the diagonal invert the colors
// second slice adds a coarse checkerboard pattern of inversion
if((offs_x != y) != (slice > 0 && (((x / 2) % 2) != ((y / 2) % 2))))
{
ret = 0.85f;
// so we can fill stencil data, clip off the inverted values
if(flags == 1)
discard;
}
// second sample/mip is shifted up a bit. MSAA textures have no mips,
// textures with mips have no samples.
ret += 0.075f * (gl_SampleID + mip);
gl_FragDepth = ret;
}
)EOSHADER";
std::string pixelMSUInt = R"EOSHADER(
#version 420 core
uniform uint texWidth;
uniform uint slice;
uniform uint mip;
uniform uint flags;
uniform uint zlayer;
layout(location = 0, index = 0) out uvec4 Color;
void main()
{
uint x = uint(gl_FragCoord.x);
uint y = uint(gl_FragCoord.y);
uvec4 ret = uvec4(10, 40, 70, 100);
// each 3D slice cycles the x. This only affects the primary diagonal
uint offs_x = (x + zlayer) % max(1u, texWidth >> mip);
// pixels off the diagonal invert the colors
if(offs_x != y)
ret = ret.wzyx;
// second slice adds a coarse checkerboard pattern of inversion
if(slice > 0 && (((x / 2) % 2) != ((y / 2) % 2)))
ret = ret.wzyx;
// second sample/mip is shifted up a bit. MSAA textures have no mips,
// textures with mips have no samples.
ret += uvec4(10, 10, 10, 10) * (gl_SampleID + mip);
Color = ret;
}
)EOSHADER";
std::string pixelMSSInt = R"EOSHADER(
#version 420 core
uniform uint texWidth;
uniform uint slice;
uniform uint mip;
uniform uint flags;
uniform uint zlayer;
layout(location = 0, index = 0) out ivec4 Color;
void main()
{
uint x = uint(gl_FragCoord.x);
uint y = uint(gl_FragCoord.y);
ivec4 ret = ivec4(10, 40, 70, 100);
// each 3D slice cycles the x. This only affects the primary diagonal
uint offs_x = (x + zlayer) % max(1u, texWidth >> mip);
// pixels off the diagonal invert the colors
if(offs_x != y)
ret = ret.wzyx;
// second slice adds a coarse checkerboard pattern of inversion
if(slice > 0 && (((x / 2) % 2) != ((y / 2) % 2)))
ret = ret.wzyx;
// second sample/mip is shifted up a bit. MSAA textures have no mips,
// textures with mips have no samples.
ret += ivec4(10 * (gl_SampleID + mip));
Color = -ret;
}
)EOSHADER";
struct GLFormat
{
const std::string name;
GLenum internalFormat;
TexConfig cfg;
};
struct TestCase
{
GLFormat fmt;
GLenum target;
uint32_t dim;
bool isArray;
bool isMSAA;
bool isRect;
bool canRender;
bool canDepth;
bool canStencil;
bool hasData;
GLuint tex;
};
std::string MakeName(const TestCase &test)
{
std::string name = "Texture " + std::to_string(test.dim) + "D";
if(test.isRect)
name = "Texture Rect";
if(test.isMSAA)
name += " MSAA";
if(test.isArray)
name += " Array";
return name;
}
GLuint GetProgram(const TestCase &test)
{
static std::map<uint32_t, GLuint> programs;
uint32_t key = uint32_t(test.fmt.cfg.data);
key |= test.dim << 6;
key |= test.isMSAA ? 0x80000 : 0;
key |= test.isArray ? 0x100000 : 0;
key |= test.isRect ? 0x200000 : 0;
GLuint ret = programs[key];
if(!ret)
{
std::string texType = "sampler" + std::to_string(test.dim) + "D";
if(test.isMSAA)
texType += "MS";
if(test.isRect)
texType += "Rect";
if(test.dim < 3 && test.isArray)
texType += "Array";
std::string typemod = "";
if(test.fmt.cfg.data == DataType::UInt)
typemod = "u";
else if(test.fmt.cfg.data == DataType::SInt)
typemod = "i";
std::string src = pixelTemplate;
uint32_t dim = test.dim + (test.isArray ? 1 : 0);
if(test.isRect)
src.replace(src.find("&params"), 7, "ivec2(0)");
else if(dim == 1)
src.replace(src.find("&params"), 7, "int(0), 0");
else if(dim == 2)
src.replace(src.find("&params"), 7, "ivec2(0), 0");
else if(dim == 3)
src.replace(src.find("&params"), 7, "ivec3(0), 0");
src.replace(src.find("&texdecl"), 8, typemod + texType);
ret = programs[key] = MakeProgram(blitVertex, src);
}
return ret;
}
bool QueryFormatBool(GLenum target, GLenum format, GLenum pname)
{
GLint param = 0;
glGetInternalformativ(target, format, pname, 4, &param);
return param != 0;
}
void FinaliseTest(TestCase & test)
{
test.canRender = QueryFormatBool(test.target, test.fmt.internalFormat, GL_COLOR_RENDERABLE);
test.canDepth = QueryFormatBool(test.target, test.fmt.internalFormat, GL_DEPTH_RENDERABLE);
test.canStencil = QueryFormatBool(test.target, test.fmt.internalFormat, GL_STENCIL_RENDERABLE);
Vec4i dimensions(texWidth, texHeight, texDepth);
bool isCompressed =
(test.fmt.cfg.type != TextureType::R9G9B9E5 && test.fmt.cfg.type != TextureType::Regular);
// Some GL drivers report that block compressed textures are supported for MSAA and color
// rendering. Save them from themselves. Similarly they report support for 1D and 3D but then it
// doesn't work properly
if(isCompressed && (test.dim == 1 || test.dim == 3 || test.isRect || test.isMSAA))
return;
// any format that supports MSAA but can't be rendered to is unsupported
if(!test.canRender && !test.canDepth && !test.canStencil && test.isMSAA)
return;
test.tex = MakeTexture();
glBindTexture(test.target, test.tex);
if(test.dim == 1)
{
if(test.isArray)
glTexStorage2D(test.target, texMips, test.fmt.internalFormat, texWidth, texSlices);
else
glTexStorage1D(test.target, texMips, test.fmt.internalFormat, texWidth);
dimensions.y = dimensions.z = 1;
}
else if(test.isRect)
{
glTexStorage2D(test.target, 1, test.fmt.internalFormat, texWidth, texHeight);
dimensions.z = 1;
}
else if(test.dim == 2)
{
if(test.isMSAA)
{
if(test.isArray)
glTexStorage3DMultisample(test.target, texSamples, test.fmt.internalFormat, texWidth,
texHeight, texSlices, GL_TRUE);
else
glTexStorage2DMultisample(test.target, texSamples, test.fmt.internalFormat, texWidth,
texHeight, GL_TRUE);
}
else
{
if(test.isArray)
glTexStorage3D(test.target, texMips, test.fmt.internalFormat, texWidth, texHeight,
texSlices);
else
glTexStorage2D(test.target, texMips, test.fmt.internalFormat, texWidth, texHeight);
}
dimensions.z = 1;
}
else if(test.dim == 3)
{
glTexStorage3D(test.target, texMips, test.fmt.internalFormat, texWidth, texHeight, texDepth);
}
if(test.canRender || test.canDepth || test.canStencil)
{
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, 0, 0);
GLenum attach = GL_COLOR_ATTACHMENT0;
if(test.canDepth && test.canStencil)
attach = GL_DEPTH_STENCIL_ATTACHMENT;
else if(test.canDepth)
attach = GL_DEPTH_ATTACHMENT;
else if(test.canStencil)
attach = GL_STENCIL_ATTACHMENT;
if(test.dim == 3 || test.isArray)
glFramebufferTextureLayer(GL_FRAMEBUFFER, attach, test.tex, 0, 0);
else
glFramebufferTexture(GL_FRAMEBUFFER, attach, test.tex, 0);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
// sometimes the GL driver lies about being able to render to a format! weeee!
if(status != GL_FRAMEBUFFER_COMPLETE)
{
test.canRender = false;
test.canDepth = false;
test.canStencil = false;
}
if(!test.canRender && !test.canDepth && !test.canStencil && test.isMSAA)
{
test.tex = 0;
return;
}
}
glObjectLabel(GL_TEXTURE, test.tex, -1, (MakeName(test) + " " + test.fmt.name).c_str());
// invalidate the texture, this makes renderdoc treat it as dirty
glInvalidateTexImage(test.tex, 0);
if(!test.isMSAA)
{
pushMarker("Set data for " + test.fmt.name + " " + MakeName(test));
test.hasData = SetData(test, dimensions);
popMarker();
}
}
bool SetData(const TestCase &test, Vec4i dim)
{
bool isCompressed =
(test.fmt.cfg.type != TextureType::R9G9B9E5 && test.fmt.cfg.type != TextureType::Regular);
TexData data;
// tightly packed data
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
GLenum format = GL_RGBA;
GLenum type = GL_UNSIGNED_BYTE;
if(test.fmt.cfg.componentBytes == 2)
type = GL_UNSIGNED_SHORT;
else if(test.fmt.cfg.componentBytes == 2)
type = GL_UNSIGNED_INT;
bool isInt = (test.fmt.cfg.data == DataType::SInt || test.fmt.cfg.data == DataType::UInt);
if(test.fmt.cfg.componentCount == 4)
format = isInt ? GL_RGBA_INTEGER : GL_RGBA;
else if(test.fmt.cfg.componentCount == 3)
format = isInt ? GL_RGB_INTEGER : GL_RGB;
else if(test.fmt.cfg.componentCount == 2)
format = isInt ? GL_RG_INTEGER : GL_RG;
else if(test.fmt.cfg.componentCount == 1)
format = isInt ? GL_RED_INTEGER : GL_RED;
if(test.fmt.cfg.type == TextureType::R9G9B9E5)
format = GL_RGB;
GLint slices = test.isArray ? texSlices : 1;
GLint mips = test.isMSAA || test.isRect ? 1 : texMips;
for(GLint s = 0; s < slices; s++)
{
for(GLint m = 0; m < mips; m++)
{
MakeData(data, test.fmt.cfg, dim, m, s);
if(data.byteData.empty())
return false;
uint32_t mipWidth = std::max(texWidth >> m, 1U);
uint32_t mipHeight = std::max(texHeight >> m, 1U);
uint32_t mipDepth = std::max(texDepth >> m, 1U);
if(isCompressed)
{
if(test.dim == 1)
{
if(test.isArray)
glCompressedTexSubImage2D(test.target, m, 0, s, mipWidth, 1, test.fmt.internalFormat,
(GLsizei)data.byteData.size(), data.byteData.data());
else
glCompressedTexSubImage1D(test.target, m, 0, mipWidth, format,
(GLsizei)data.byteData.size(), data.byteData.data());
}
else if(test.isRect)
{
glCompressedTexSubImage2D(test.target, 0, 0, 0, mipWidth, mipHeight,
test.fmt.internalFormat, (GLsizei)data.byteData.size(),
data.byteData.data());
}
else if(test.dim == 2)
{
if(test.isArray)
glCompressedTexSubImage3D(test.target, m, 0, 0, s, mipWidth, mipHeight, 1,
test.fmt.internalFormat, (GLsizei)data.byteData.size(),
data.byteData.data());
else
glCompressedTexSubImage2D(test.target, m, 0, 0, mipWidth, mipHeight,
test.fmt.internalFormat, (GLsizei)data.byteData.size(),
data.byteData.data());
}
else if(test.dim == 3)
{
glCompressedTexSubImage3D(test.target, m, 0, 0, 0, mipWidth, mipHeight, mipDepth,
test.fmt.internalFormat, (GLsizei)data.byteData.size(),
data.byteData.data());
}
}
else
{
if(test.dim == 1)
{
if(test.isArray)
glTexSubImage2D(test.target, m, 0, s, mipWidth, 1, format, type, data.byteData.data());
else
glTexSubImage1D(test.target, m, 0, mipWidth, format, type, data.byteData.data());
}
else if(test.isRect)
{
glTexSubImage2D(test.target, 0, 0, 0, mipWidth, mipHeight, format, type,
data.byteData.data());
}
else if(test.dim == 2)
{
if(test.isArray)
glTexSubImage3D(test.target, m, 0, 0, s, mipWidth, mipHeight, 1, format, type,
data.byteData.data());
else
glTexSubImage2D(test.target, m, 0, 0, mipWidth, mipHeight, format, type,
data.byteData.data());
}
else if(test.dim == 3)
{
glTexSubImage3D(test.target, m, 0, 0, 0, mipWidth, mipHeight, mipDepth, format, type,
data.byteData.data());
}
}
}
}
return true;
}
void AddSupportedTests(const GLFormat &f, std::vector<TestCase> &test_textures, bool depthMode)
{
// TODO: disable 1D depth textures for now, we don't support displaying them
if(!depthMode)
{
test_textures.push_back({f, GL_TEXTURE_1D, 1, false});
test_textures.push_back({f, GL_TEXTURE_1D_ARRAY, 1, true});
}
test_textures.push_back({f, GL_TEXTURE_2D, 2, false});
test_textures.push_back({f, GL_TEXTURE_2D_ARRAY, 2, true});
test_textures.push_back({f, GL_TEXTURE_3D, 3, false});
// TODO: we don't support MSAA<->Array copies for these odd sized pixels, and I suspect drivers
// emulate the formats anyway. Disable for now
if(f.cfg.type != TextureType::Regular || f.cfg.componentCount != 3)
{
test_textures.push_back({f, GL_TEXTURE_2D_MULTISAMPLE, 2, false, true});
test_textures.push_back({f, GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 2, true, true});
}
test_textures.push_back({f, GL_TEXTURE_RECTANGLE, 2, false, false, true});
}
int main()
{
// initialise, create window, create context, etc
if(!Init())
return 3;
GLuint vao = MakeVAO();
glBindVertexArray(vao);
pushMarker("Add tests");
#define TEST_CASE_NAME(texFmt) #texFmt
#define TEST_CASE(texType, texFmt, compCount, byteWidth, dataType) \
{ \
#texFmt + 3, texFmt, {texType, compCount, byteWidth, dataType }, \
}
std::vector<TestCase> test_textures;
const GLFormat color_tests[] = {
TEST_CASE(TextureType::Regular, GL_RGBA32F, 4, 4, DataType::Float),
TEST_CASE(TextureType::Regular, GL_RGBA32UI, 4, 4, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_RGBA32I, 4, 4, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_RGB32F, 3, 4, DataType::Float),
TEST_CASE(TextureType::Regular, GL_RGB32UI, 3, 4, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_RGB32I, 3, 4, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_RG32F, 2, 4, DataType::Float),
TEST_CASE(TextureType::Regular, GL_RG32UI, 2, 4, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_RG32I, 2, 4, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_R32F, 1, 4, DataType::Float),
TEST_CASE(TextureType::Regular, GL_R32UI, 1, 4, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_R32I, 1, 4, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_RGBA16F, 4, 2, DataType::Float),
TEST_CASE(TextureType::Regular, GL_RGBA16UI, 4, 2, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_RGBA16I, 4, 2, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_RGBA16, 4, 2, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_RGBA16_SNORM, 4, 2, DataType::SNorm),
TEST_CASE(TextureType::Regular, GL_RGB16F, 3, 2, DataType::Float),
TEST_CASE(TextureType::Regular, GL_RGB16UI, 3, 2, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_RGB16I, 3, 2, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_RGB16, 3, 2, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_RGB16_SNORM, 3, 2, DataType::SNorm),
TEST_CASE(TextureType::Regular, GL_RG16F, 2, 2, DataType::Float),
TEST_CASE(TextureType::Regular, GL_RG16UI, 2, 2, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_RG16I, 2, 2, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_RG16, 2, 2, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_RG16_SNORM, 2, 2, DataType::SNorm),
TEST_CASE(TextureType::Regular, GL_R16F, 1, 2, DataType::Float),
TEST_CASE(TextureType::Regular, GL_R16UI, 1, 2, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_R16I, 1, 2, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_R16, 1, 2, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_R16_SNORM, 1, 2, DataType::SNorm),
TEST_CASE(TextureType::Regular, GL_RGBA8UI, 4, 1, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_RGBA8I, 4, 1, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_RGBA8, 4, 1, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_SRGB8_ALPHA8, 4, 1, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_RGBA8_SNORM, 4, 1, DataType::SNorm),
TEST_CASE(TextureType::Regular, GL_RGB8UI, 3, 1, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_RGB8I, 3, 1, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_RGB8, 3, 1, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_SRGB8, 3, 1, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_RGB8_SNORM, 3, 1, DataType::SNorm),
TEST_CASE(TextureType::Regular, GL_RG8UI, 2, 1, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_RG8I, 2, 1, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_RG8, 2, 1, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_SRG8_EXT, 1, 1, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_RG8_SNORM, 2, 1, DataType::SNorm),
TEST_CASE(TextureType::Regular, GL_R8UI, 1, 1, DataType::UInt),
TEST_CASE(TextureType::Regular, GL_R8I, 1, 1, DataType::SInt),
TEST_CASE(TextureType::Regular, GL_R8, 1, 1, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_SR8_EXT, 1, 1, DataType::UNorm),
TEST_CASE(TextureType::Regular, GL_R8_SNORM, 1, 1, DataType::SNorm),
TEST_CASE(TextureType::Unknown, GL_RGB565, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::Unknown, GL_RGB5_A1, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::Unknown, GL_RGB10_A2, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::Unknown, GL_RGB10_A2UI, 0, 0, DataType::UInt),
TEST_CASE(TextureType::Unknown, GL_RGBA4, 0, 0, DataType::UNorm),
// formats we don't support in RenderDoc currently
// TEST_CASE(TextureType::Unknown, GL_RGB4, 0, 0, DataType::UNorm),
// TEST_CASE(TextureType::Unknown, GL_RGB5, 0, 0, DataType::UNorm),
// TEST_CASE(TextureType::Unknown, GL_RGB10, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::Unknown, GL_R11F_G11F_B10F, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::R9G9B9E5, GL_RGB9_E5, 0, 0, DataType::Float),
TEST_CASE(TextureType::BC1, GL_COMPRESSED_RGB_S3TC_DXT1_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC1, GL_COMPRESSED_SRGB_S3TC_DXT1_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC1, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC1, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC2, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC2, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC3, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC3, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC4, GL_COMPRESSED_RED_RGTC1_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC4, GL_COMPRESSED_SIGNED_RED_RGTC1_EXT, 0, 0, DataType::SNorm),
TEST_CASE(TextureType::BC5, GL_COMPRESSED_RED_GREEN_RGTC2_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC5, GL_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT, 0, 0, DataType::SNorm),
TEST_CASE(TextureType::BC6, GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC6, GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT, 0, 0, DataType::SNorm),
TEST_CASE(TextureType::BC7, GL_COMPRESSED_RGBA_BPTC_UNORM, 0, 0, DataType::UNorm),
TEST_CASE(TextureType::BC7, GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, 0, 0, DataType::UNorm),
};
for(GLFormat f : color_tests)
{
if(f.internalFormat == GL_SR8_EXT && !GLAD_GL_EXT_texture_sRGB_R8)
continue;
if(f.internalFormat == GL_SRG8_EXT && !GLAD_GL_EXT_texture_sRGB_RG8)
continue;
AddSupportedTests(f, test_textures, false);
}
// finally add the depth tests
const GLFormat depth_tests[] = {
TEST_CASE(TextureType::Unknown, GL_DEPTH32F_STENCIL8, 0, 0, DataType::Float),
TEST_CASE(TextureType::Unknown, GL_DEPTH_COMPONENT32F, 0, 0, DataType::Float),
TEST_CASE(TextureType::Unknown, GL_DEPTH24_STENCIL8, 0, 0, DataType::Float),
TEST_CASE(TextureType::Unknown, GL_DEPTH_COMPONENT24, 0, 0, DataType::Float),
TEST_CASE(TextureType::Unknown, GL_DEPTH_COMPONENT16, 0, 0, DataType::Float),
};
for(GLFormat f : depth_tests)
AddSupportedTests(f, test_textures, true);
GLuint renderFBO = MakeFBO();
glBindFramebuffer(GL_FRAMEBUFFER, renderFBO);
for(TestCase &t : test_textures)
{
if(QueryFormatBool(t.target, t.fmt.internalFormat, GL_INTERNALFORMAT_SUPPORTED) &&
QueryFormatBool(t.target, t.fmt.internalFormat, GL_FRAGMENT_TEXTURE))
{
FinaliseTest(t);
}
}
popMarker();
GLuint msprog[(size_t)DataType::Count];
msprog[(size_t)DataType::Float] = msprog[(size_t)DataType::UNorm] =
msprog[(size_t)DataType::SNorm] = MakeProgram(blitVertex, pixelMSFloat);
msprog[(size_t)DataType::UInt] = MakeProgram(blitVertex, pixelMSUInt);
msprog[(size_t)DataType::SInt] = MakeProgram(blitVertex, pixelMSSInt);
GLuint msdepthprog = MakeProgram(blitVertex, pixelMSDepth);
for(TestCase &t : test_textures)
{
if(!t.tex || t.hasData)
continue;
if(!t.canRender && !t.canDepth && !t.canStencil)
{
TEST_ERROR("Need data for test %s, but it's not a renderable/depthable format",
t.fmt.name.c_str());
continue;
}
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, 0, 0);
if(t.canDepth || t.canStencil)
{
glEnable(GL_DEPTH_TEST);
glEnable(GL_STENCIL_TEST);
glDepthMask(0xff);
glDepthFunc(GL_ALWAYS);
glStencilFunc(GL_ALWAYS, 0, 0xff);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
}
else
{
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
}
pushMarker("Render data for " + t.fmt.name + " " + MakeName(t));
t.hasData = true;
bool srgb = false;
switch(t.fmt.internalFormat)
{
// only need to handle renderable SRGB formats here
case GL_SRGB8:
case GL_SRGB8_ALPHA8: srgb = true; break;
default: break;
}
int flags = 0;
if(t.fmt.cfg.data == DataType::SNorm)
flags |= 1;
if(srgb)
flags |= 2;
GLuint slices = t.isArray ? texSlices : 1u;
GLuint mips = t.isMSAA || t.isRect ? 1u : texMips;
for(GLuint mp = 0; mp < mips; mp++)
{
GLuint SlicesOrDepth = slices;
if(t.dim == 3)
SlicesOrDepth >>= mp;
for(GLuint sl = 0; sl < SlicesOrDepth; sl++)
{
if(t.canDepth || t.canStencil)
{
GLenum attach = GL_NONE;
if(t.canDepth && t.canStencil)
attach = GL_DEPTH_STENCIL_ATTACHMENT;
else if(t.canDepth)
attach = GL_DEPTH_ATTACHMENT;
else if(t.canStencil)
attach = GL_STENCIL_ATTACHMENT;
if(t.dim == 3 || t.isArray)
glFramebufferTextureLayer(GL_FRAMEBUFFER, attach, t.tex, mp, sl);
else
glFramebufferTexture(GL_FRAMEBUFFER, attach, t.tex, mp);
glClearBufferfi(GL_DEPTH_STENCIL, 0, 0.0, 0);
GLuint p = msdepthprog;
glUseProgram(p);
glUniform1ui(glGetUniformLocation(p, "texWidth"), texWidth);
glUniform1ui(glGetUniformLocation(p, "slice"), sl);
glUniform1ui(glGetUniformLocation(p, "mip"), mp);
glUniform1ui(glGetUniformLocation(p, "flags"), flags);
glUniform1ui(glGetUniformLocation(p, "zlayer"), t.dim == 3 ? sl : 0);
glViewport(0, 0, texWidth, texHeight);
uint32_t sampleCount = t.isMSAA ? texSamples : 1;
// need to do each sample separately to let us vary the stencil value
for(uint32_t sm = 0; sm < sampleCount; sm++)
{
glSampleMaski(0, 1 << sm);
glStencilFunc(GL_ALWAYS, 100 + (mp + sm) * 10, 0xff);
glDrawArrays(GL_TRIANGLES, 0, 3);
// clip off the diagonal
glUniform1ui(glGetUniformLocation(p, "flags"), 1);
glStencilFunc(GL_ALWAYS, 10 + (mp + sm) * 10, 0xff);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
}
else
{
if(t.dim == 3 || t.isArray)
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, t.tex, mp, sl);
else
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, t.tex, mp);
GLuint p = msprog[(size_t)t.fmt.cfg.data];
glUseProgram(p);
glUniform1ui(glGetUniformLocation(p, "texWidth"), texWidth);
glUniform1ui(glGetUniformLocation(p, "slice"), t.dim == 3 ? 0 : sl);
glUniform1ui(glGetUniformLocation(p, "mip"), mp);
glUniform1ui(glGetUniformLocation(p, "flags"), flags);
glUniform1ui(glGetUniformLocation(p, "zlayer"), t.dim == 3 ? sl : 0);
glViewport(0, 0, texWidth, texHeight);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
}
}
popMarker();
}
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
GLuint fbo = MakeFBO();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// Color render texture
GLuint colattach = MakeTexture();
glBindTexture(GL_TEXTURE_2D, colattach);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, screenWidth, screenHeight);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colattach, 0);
while(Running())
{
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
float col[] = {0.4f, 0.5f, 0.6f, 1.0f};
glClearBufferfv(GL_COLOR, 0, col);
glBindVertexArray(vao);
GLsizei viewX = 0, viewY = screenHeight - 10;
glEnable(GL_SCISSOR_TEST);
for(size_t i = 0; i < test_textures.size(); i++)
{
if(i == 0 || test_textures[i].fmt.internalFormat != test_textures[i - 1].fmt.internalFormat)
{
if(i != 0)
popMarker();
pushMarker(test_textures[i].fmt.name);
}
setMarker(MakeName(test_textures[i]));
glViewport(viewX, viewY, 10, 10);
glScissor(viewX + 1, viewY + 1, 8, 8);
glUseProgram(GetProgram(test_textures[i]));
if(test_textures[i].tex)
{
glBindTextureUnit(0, test_textures[i].tex);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
else
{
setMarker("UNSUPPORTED");
}
// advance to next viewport
viewX += 10;
if(viewX + 10 > (float)screenWidth)
{
viewX = 0;
viewY -= 10;
}
}
// pop the last format region
popMarker();
glViewport(0, 0, GLsizei(screenWidth), GLsizei(screenHeight));
glDisable(GL_SCISSOR_TEST);
// blit to the screen for a nicer preview
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, screenWidth, screenHeight, 0, 0, screenWidth, screenHeight,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
Present();
}
return 0;
}
};
REGISTER_TEST();
+1 -1
View File
@@ -492,4 +492,4 @@ bool GraphicsTest::FrameLimit()
return false;
return true;
}
}
+61
View File
@@ -121,6 +121,8 @@ struct Vec4f
z = Z;
w = W;
}
bool operator==(const Vec4f &o) { return x == o.x && y == o.y && z == o.z && w == o.w; }
bool operator!=(const Vec4f &o) { return !(*this == o); }
float x, y, z, w;
};
@@ -333,3 +335,62 @@ void DebugPrint(const char *fmt, ...);
DEBUG_BREAK(); \
exit(0); \
} while(0)
namespace TextureZoo
{
enum class DataType
{
Float,
UNorm,
SNorm,
UInt,
SInt,
Count,
};
enum class TextureType
{
Unknown,
Regular,
R9G9B9E5,
G4R4,
A4R4G4B4,
R4G4B4A4,
R5G6B5,
R5G5B5A1,
A1R5G5B5,
RGB10A2,
BC1,
BC2,
BC3,
BC4,
BC5,
BC6,
BC7,
};
static const uint32_t texWidth = 8;
static const uint32_t texHeight = 8;
static const uint32_t texDepth = 10;
static const uint32_t texMips = 3;
static const uint32_t texSlices = 2;
static const uint32_t texSamples = 2;
struct TexConfig
{
TextureType type;
uint32_t componentCount;
uint32_t componentBytes;
DataType data;
};
struct TexData
{
std::vector<byte> byteData;
uint32_t rowPitch = 0;
uint32_t slicePitch = 0;
};
void MakeData(TexData &data, const TexConfig &cfg, Vec4i dimensions, uint32_t mip, uint32_t slice);
}; // namespace TextureZoo
+820
View File
@@ -0,0 +1,820 @@
/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#include <algorithm>
#include "test_common.h"
namespace TextureZoo
{
// quick and dirty half conversion, doesn't handle NaN/inf/subnormal/truncation/rounding
uint16_t MakeHalf(float f)
{
bool sign = f < 0.0f;
f = sign ? -f : f;
if(f < 1e-15f)
return 0;
int exp;
f = frexpf(f, &exp);
uint32_t mantissa;
memcpy(&mantissa, &f, sizeof(mantissa));
mantissa = (mantissa & 0x007fffff) >> 13;
uint16_t ret = mantissa & 0x3ff;
ret |= ((exp + 14) << 10);
if(sign)
ret |= 0x8000;
return ret;
}
void MakePixel(byte *data, const TexConfig &cfg, uint32_t x, uint32_t y, uint32_t z, uint32_t mip,
uint32_t slice)
{
// each 3D slice cycles the x
x += z;
x %= std::max(1U, texWidth >> mip);
if(cfg.data == DataType::Float || cfg.data == DataType::UNorm || cfg.data == DataType::SNorm)
{
// start points for each component
const float vals[] = {
0.1f, 0.35f, 0.6f, 0.85f,
};
for(uint32_t c = 0; c < cfg.componentCount; c++)
{
uint32_t idx = c;
// pixels off the diagonal invert the colors
if(x != y)
idx = 3 - idx;
// subsequent slices add a coarse checkerboard pattern of inverted colors
if((slice % 3 > 0) && (((x / 2) % 2) != ((y / 2) % 2)))
idx = 3 - idx;
float f = vals[idx];
// subsequent mips are shifted up a bit
f += 0.075f * mip;
// Signed normals are negative
if(cfg.data == DataType::SNorm)
f = -f;
// if it's a full float, just copy
if(cfg.componentBytes == 4)
{
memcpy(data, &f, cfg.componentBytes);
}
else if(cfg.componentBytes == 2)
{
uint16_t h;
if(cfg.data == DataType::Float)
h = MakeHalf(f);
else if(cfg.data == DataType::UNorm)
h = uint16_t(f * 0xffff);
else if(cfg.data == DataType::SNorm)
h = int16_t(f * 0x7fff);
memcpy(data, &h, cfg.componentBytes);
}
else if(cfg.componentBytes == 1)
{
uint8_t b;
if(cfg.data == DataType::UNorm)
b = uint8_t(f * 0xff);
else if(cfg.data == DataType::SNorm)
b = int8_t(f * 0x7f);
memcpy(data, &b, cfg.componentBytes);
}
else
{
TEST_ERROR("Unexpected component bytes %d in float", cfg.componentBytes);
}
data += cfg.componentBytes;
}
}
else if(cfg.data == DataType::UInt || cfg.data == DataType::SInt)
{
// same pattern as above but with integer values
const int32_t vals[] = {
10, 40, 70, 100,
};
for(uint32_t c = 0; c < cfg.componentCount; c++)
{
uint32_t idx = c;
// pixels off the diagonal invert the colors
if(x != y)
idx = 3 - idx;
if((slice % 3 > 0) && (((x / 2) % 2) != ((y / 2) % 2)))
idx = 3 - idx;
int32_t val = vals[idx];
val += 10 * mip;
// Signed ints are negative
if(cfg.data == DataType::SInt)
val = -val;
// because the values are below one byte and we're little-endian we can just copy the
// right number of bytes from val
memcpy(data, &val, cfg.componentBytes);
data += cfg.componentBytes;
}
}
}
void MakeData(TexData &data, const TexConfig &cfg, Vec4i dimensions, uint32_t mip, uint32_t slice)
{
uint32_t mipWidth = std::max(1, dimensions.x >> mip);
uint32_t mipHeight = std::max(1, dimensions.y >> mip);
uint32_t mipDepth = std::max(1, dimensions.z >> mip);
if(cfg.type == TextureType::Unknown)
{
data = TexData();
return;
}
else if(cfg.type == TextureType::Regular)
{
uint32_t pixelPitch = cfg.componentBytes * cfg.componentCount;
data.rowPitch = pixelPitch * mipWidth;
data.slicePitch = data.rowPitch * mipHeight;
data.byteData.resize(data.slicePitch * mipDepth);
byte *out = data.byteData.data();
for(uint32_t z = 0; z < mipDepth; z++)
{
for(uint32_t y = 0; y < mipHeight; y++)
{
for(uint32_t x = 0; x < mipWidth; x++)
{
MakePixel(out, cfg, x, y, z, mip, slice);
out += pixelPitch;
}
}
}
}
else
{
bool bc1 = false, bc2alpha = false, bc3alpha = false, bc6 = false, bc7 = false, sharedExp = false;
int bc4channels = 0;
uint32_t nybblePattern = 0;
bool rgb5 = false;
int alphabitPlace = 0;
bool rgb10a2 = false;
switch(cfg.type)
{
case TextureType::BC1: bc1 = true; break;
case TextureType::BC2:
bc1 = true;
bc2alpha = true;
break;
case TextureType::BC3:
bc1 = true;
bc3alpha = true;
break;
case TextureType::BC4: bc4channels = 1; break;
case TextureType::BC5: bc4channels = 2; break;
case TextureType::BC6: bc6 = true; break;
case TextureType::BC7: bc7 = true; break;
case TextureType::R9G9B9E5: sharedExp = true; break;
case TextureType::G4R4: nybblePattern = 0x12; break;
case TextureType::A4R4G4B4: nybblePattern = 0x1234; break;
case TextureType::R4G4B4A4: nybblePattern = 0x4321; break;
case TextureType::R5G6B5:
rgb5 = true;
alphabitPlace = 0;
break;
case TextureType::R5G5B5A1:
rgb5 = true;
alphabitPlace = 1;
break;
case TextureType::A1R5G5B5:
rgb5 = true;
alphabitPlace = 2;
break;
case TextureType::RGB10A2: rgb10a2 = true; break;
default: data = TexData(); return;
}
// get float data so we can do the best possible job of truncating to the desired bit width
TexConfig floatcfg = {TextureType::Regular, 4, 4, DataType::Float};
TexData floatdata;
if(rgb10a2 && cfg.data == DataType::UInt)
floatcfg.data = cfg.data;
MakeData(floatdata, floatcfg, dimensions, mip, slice);
Vec4f *srcPixels = (Vec4f *)floatdata.byteData.data();
Vec4i *srcPixelsI = (Vec4i *)floatdata.byteData.data();
if(rgb10a2)
{
uint32_t pixelPitch = 4;
data.rowPitch = pixelPitch * mipWidth;
data.slicePitch = data.rowPitch * mipHeight;
data.byteData.resize(data.slicePitch * mipDepth);
uint32_t *out = (uint32_t *)data.byteData.data();
for(uint32_t z = 0; z < mipDepth; z++)
{
for(uint32_t y = 0; y < mipHeight; y++)
{
for(uint32_t x = 0; x < mipWidth; x++)
{
uint32_t encodedPixel = 0;
if(cfg.data == DataType::UInt)
{
int32_t rgba[4];
rgba[0] = srcPixelsI[y * mipWidth + x].x;
rgba[1] = srcPixelsI[y * mipWidth + x].y;
rgba[2] = srcPixelsI[y * mipWidth + x].z;
rgba[3] = srcPixelsI[y * mipWidth + x].w;
encodedPixel |= (rgba[0] & 0x3ff) << 0;
encodedPixel |= (rgba[1] & 0x3ff) << 10;
encodedPixel |= (rgba[2] & 0x3ff) << 20;
encodedPixel |= (std::min(rgba[3], 3) & 0x3) << 30;
}
else
{
float rgba[4];
rgba[0] = srcPixels[y * mipWidth + x].x;
rgba[1] = srcPixels[y * mipWidth + x].y;
rgba[2] = srcPixels[y * mipWidth + x].z;
rgba[3] = srcPixels[y * mipWidth + x].w;
encodedPixel |= uint32_t(round(rgba[0] * 0x3ff)) << 0;
encodedPixel |= uint32_t(round(rgba[1] * 0x3ff)) << 10;
encodedPixel |= uint32_t(round(rgba[2] * 0x3ff)) << 20;
encodedPixel |= uint32_t(round(rgba[3] * 0x3)) << 30;
}
*out = encodedPixel;
out++;
}
}
srcPixels += mipWidth * mipHeight;
srcPixelsI += mipWidth * mipHeight;
}
}
else if(nybblePattern || rgb5)
{
uint32_t pixelPitch = 2;
data.rowPitch = pixelPitch * mipWidth;
data.slicePitch = data.rowPitch * mipHeight;
data.byteData.resize(data.slicePitch * mipDepth);
uint8_t *out = data.byteData.data();
for(uint32_t z = 0; z < mipDepth; z++)
{
for(uint32_t y = 0; y < mipHeight; y++)
{
for(uint32_t x = 0; x < mipWidth; x++)
{
float rgb[4];
rgb[0] = srcPixels[y * mipWidth + x].x;
rgb[1] = srcPixels[y * mipWidth + x].y;
rgb[2] = srcPixels[y * mipWidth + x].z;
rgb[3] = srcPixels[y * mipWidth + x].w;
if(rgb5)
{
bool alpha = rgb[3] >= 0.5f;
uint16_t encodedPixel = 0;
if(alphabitPlace == 0)
{
encodedPixel |= uint16_t(rgb[2] * 31) << 0;
encodedPixel |= uint16_t(rgb[1] * 63) << 5;
encodedPixel |= uint16_t(rgb[0] * 31) << 11;
}
else
{
encodedPixel |= uint16_t(rgb[2] * 31) << 0;
encodedPixel |= uint16_t(rgb[1] * 31) << 5;
encodedPixel |= uint16_t(rgb[0] * 31) << 10;
if(alphabitPlace == 1)
{
if(alpha)
encodedPixel |= 0x8000;
}
else
{
encodedPixel <<= 1;
if(alpha)
encodedPixel |= 0x1;
}
}
memcpy(out, &encodedPixel, sizeof(encodedPixel));
out += 2;
}
else
{
uint8_t encodedPixel = 0;
encodedPixel |= uint8_t(rgb[((nybblePattern & 0x000f) >> 0) - 1] * 15) << 0;
encodedPixel |= uint8_t(rgb[((nybblePattern & 0x00f0) >> 4) - 1] * 15) << 4;
*out = encodedPixel;
out++;
if(nybblePattern & 0xff00)
{
encodedPixel = 0;
encodedPixel |= uint8_t(rgb[((nybblePattern & 0x0f00) >> 8) - 1] * 15) << 0;
encodedPixel |= uint8_t(rgb[((nybblePattern & 0xf000) >> 12) - 1] * 15) << 4;
*out = encodedPixel;
out++;
}
}
}
}
srcPixels += mipWidth * mipHeight;
}
}
else if(sharedExp)
{
uint32_t pixelPitch = 4;
data.rowPitch = pixelPitch * mipWidth;
data.slicePitch = data.rowPitch * mipHeight;
data.byteData.resize(data.slicePitch * mipDepth);
uint32_t *out = (uint32_t *)data.byteData.data();
for(uint32_t z = 0; z < mipDepth; z++)
{
for(uint32_t y = 0; y < mipHeight; y++)
{
for(uint32_t x = 0; x < mipWidth; x++)
{
float rgb[3];
rgb[0] = srcPixels[y * mipWidth + x].x;
rgb[1] = srcPixels[y * mipWidth + x].y;
rgb[2] = srcPixels[y * mipWidth + x].z;
uint32_t encodedPixel = 0;
int exp = -10;
// we pick the highest exponent, losing bits off the bottom of any value that
// needs a lower one, rather than picking a lower one and having to saturate
// values that need a higher one
for(int channel = 0; channel < 3; channel++)
{
int e = 0;
frexpf(rgb[channel], &e);
exp = std::max(exp, e);
}
for(int channel = 0; channel < 3; channel++)
encodedPixel |= uint32_t(rgb[channel] * 511.0 / (1 << exp)) << (9 * channel);
encodedPixel |= (exp + 15) << 27;
*out = encodedPixel;
out++;
}
}
srcPixels += mipWidth * mipHeight;
}
}
else
{
// these don't change, but make the code easier to read
const uint32_t blockWidth = 4;
const uint32_t blockHeight = 4;
uint32_t blockSize;
// 0.5 byte per pixel
if(cfg.type == TextureType::BC1 || cfg.type == TextureType::BC4)
blockSize = 8;
else
blockSize = 16;
data.rowPitch = blockSize * std::max(1U, mipWidth / blockWidth);
data.slicePitch = data.rowPitch * std::max(1U, mipHeight / blockHeight);
data.byteData.resize(data.slicePitch * mipDepth);
byte *out = (byte *)data.byteData.data();
const Vec4f invalid(999001.0f, 999002.0f, -999003.0f, -999004.0f);
// compress each slice separately
for(uint32_t z = 0; z < mipDepth; z++)
{
// block compressed - iterate over the pixels in block size
for(uint32_t y = 0; y < mipHeight; y += blockHeight)
{
for(uint32_t x = 0; x < mipWidth; x += blockWidth)
{
Vec4f blockPixels[blockWidth * blockHeight] = {};
// copy all the in-range pixels into the block data
for(uint32_t by = 0; by < blockHeight; by++)
{
for(uint32_t bx = 0; bx < blockWidth; bx++)
{
if(x + bx >= mipWidth || y + by >= mipHeight)
{
blockPixels[by * blockWidth + bx] = invalid;
}
else
{
blockPixels[by * blockWidth + bx] = srcPixels[(y + by) * mipWidth + (x + bx)];
}
}
}
// we should have at most two unique pixels. The pattern is structured to allow
// that, since any other colour can't be uniquely represented in all compressed
// formats (even interpolated values)
Vec4f a = invalid, b = invalid;
uint32_t bc1bitmask = 0;
uint64_t bc4bitmask = 0;
// BC1 and BC4 both share A = 0 and B = 0 codes
enum class BCCode : uint64_t
{
A = 0,
B = 1,
};
// iterate the pixels in the block in ascending bitmask order
for(uint32_t p = 0; p < blockWidth * blockHeight; p++)
{
if(blockPixels[p] == invalid)
{
// out of bounds pixel (think of a 2x2 mip), store as A - whatever A is.
bc1bitmask |= uint32_t(BCCode::A) << (p * 2);
bc4bitmask |= uint64_t(BCCode::A) << (p * 3);
}
else if(a == invalid)
{
// A hasn't been found yet, let's use this pixel for that
a = blockPixels[p];
bc1bitmask |= uint32_t(BCCode::A) << (p * 2);
bc4bitmask |= uint64_t(BCCode::A) << (p * 3);
}
else if(blockPixels[p] == a)
{
// if A has been found then re-use it before assigning to B
bc1bitmask |= uint32_t(BCCode::A) << (p * 2);
bc4bitmask |= uint64_t(BCCode::A) << (p * 3);
}
else if(b == invalid)
{
// B hasn't been found yet, let's use this pixel for that
b = blockPixels[p];
bc1bitmask |= uint32_t(BCCode::B) << (p * 2);
bc4bitmask |= uint64_t(BCCode::B) << (p * 3);
}
else if(blockPixels[p] == b)
{
bc1bitmask |= uint32_t(BCCode::B) << (p * 2);
bc4bitmask |= uint64_t(BCCode::B) << (p * 3);
}
else
{
TEST_ERROR("Found pixel that isn't A, or B!");
}
}
byte a8[4], b8[4];
uint16_t aHalf[4], bHalf[4];
int16_t *aHalfS = (int16_t *)aHalf;
int16_t *bHalfS = (int16_t *)bHalf;
uint16_t a565 = 0;
uint16_t b565 = 0;
if(cfg.data == DataType::SNorm)
{
int8_t *ia8 = (int8_t *)a8;
int8_t *ib8 = (int8_t *)b8;
ia8[0] = int8_t(round(a.x * -127.0f));
ia8[1] = int8_t(round(a.y * -127.0f));
ia8[2] = int8_t(round(a.z * -127.0f));
ia8[3] = int8_t(round(a.w * -127.0f));
ib8[0] = int8_t(round(b.x * -127.0f));
ib8[1] = int8_t(round(b.y * -127.0f));
ib8[2] = int8_t(round(b.z * -127.0f));
ib8[3] = int8_t(round(b.w * -127.0f));
aHalf[0] = MakeHalf(-a.x);
aHalf[1] = MakeHalf(-a.y);
aHalf[2] = MakeHalf(-a.z);
aHalf[3] = MakeHalf(-a.w);
bHalf[0] = MakeHalf(-b.x);
bHalf[1] = MakeHalf(-b.y);
bHalf[2] = MakeHalf(-b.z);
bHalf[3] = MakeHalf(-b.w);
}
else
{
a8[0] = byte(round(a.x * 255.0f));
a8[1] = byte(round(a.y * 255.0f));
a8[2] = byte(round(a.z * 255.0f));
a8[3] = byte(round(a.w * 255.0f));
// red
a565 |= byte(round(a.x * 31.0f)) << 11;
// green
a565 |= byte(round(a.y * 63.0f)) << 5;
// blue
a565 |= byte(round(a.z * 31.0f)) << 0;
b8[0] = byte(round(b.x * 255.0f));
b8[1] = byte(round(b.y * 255.0f));
b8[2] = byte(round(b.z * 255.0f));
b8[3] = byte(round(b.w * 255.0f));
// red
b565 |= byte(round(b.x * 31.0f)) << 11;
// green
b565 |= byte(round(b.y * 63.0f)) << 5;
// blue
b565 |= byte(round(b.z * 31.0f)) << 0;
aHalf[0] = MakeHalf(a.x);
aHalf[1] = MakeHalf(a.y);
aHalf[2] = MakeHalf(a.z);
aHalf[3] = MakeHalf(a.w);
bHalf[0] = MakeHalf(b.x);
bHalf[1] = MakeHalf(b.y);
bHalf[2] = MakeHalf(b.z);
bHalf[3] = MakeHalf(b.w);
}
struct BC1
{
uint16_t a565;
uint16_t b565;
uint32_t bitmask;
};
static_assert(sizeof(BC1) == 8, "BC1 struct is mis-sized");
struct BC4
{
uint64_t a : 8;
uint64_t b : 8;
uint64_t bitmask : 48;
};
static_assert(sizeof(BC4) == 8, "BC4 struct is mis-sized");
if(bc2alpha)
{
uint64_t alphaBits = 0;
for(uint32_t p = 0; p < blockWidth * blockHeight; p++)
{
BCCode code = BCCode((bc1bitmask & (0x3 << (p * 2))) >> (p * 2));
if(code == BCCode::A)
alphaBits |= uint64_t(a8[3] >> 4) << (p * 4);
else if(code == BCCode::B)
alphaBits |= uint64_t(b8[3] >> 4) << (p * 4);
}
memcpy(out, &alphaBits, sizeof(alphaBits));
out += sizeof(alphaBits);
}
else if(bc3alpha)
{
// basically the same layout just a different meaning for codes above 1, which
// we
// don't use
BC4 *alpha = (BC4 *)out;
alpha->a = a8[3];
alpha->b = b8[3];
alpha->bitmask = bc4bitmask;
out += sizeof(BC4);
}
if(bc1)
{
BC1 *rgb = (BC1 *)out;
// we don't care about color0 <= color1 order
rgb->a565 = a565;
rgb->b565 = b565;
rgb->bitmask = bc1bitmask;
out += sizeof(BC1);
}
for(int ch = 0; ch < bc4channels; ch++)
{
BC4 *alpha = (BC4 *)out;
alpha->a = a8[ch];
alpha->b = b8[ch];
alpha->bitmask = bc4bitmask;
out += sizeof(BC4);
}
uint64_t bc67indexbits = 0;
if(bc6 || bc7)
{
for(uint32_t p = 0; p < blockWidth * blockHeight; p++)
{
BCCode code = BCCode((bc1bitmask & (0x3 << (p * 2))) >> (p * 2));
if(p == 0)
{
// the first colour we came across should have been assigned code A. We
// require this, because we're missing a bit from the first index
TEST_ASSERT(code == BCCode::A, "First code must be code A when encoding BC6");
}
else
{
if(code == BCCode::A)
{
bc67indexbits |= uint64_t(0) << ((p * 4) - 1);
}
else if(code == BCCode::B)
{
bc67indexbits |= uint64_t(15) << ((p * 4) - 1);
}
}
}
}
if(bc6)
{
byte mode = 0x03;
// mode 3: no transformed endpoints, 0 partition bits, 10 endpoint bits per
// channel, no delta bits.
uint16_t bias = 0;
if(cfg.data == DataType::SNorm)
{
// final quantize step, the absolute value gets scaled a little
for(int ch = 0; ch < 3; ch++)
{
bool negA = (aHalf[ch] & 0x8000) != 0;
bool negB = (bHalf[ch] & 0x8000) != 0;
int16_t valA = int16_t(((aHalf[ch] & 0x7fff) * 32) / 31);
int16_t valB = int16_t(((bHalf[ch] & 0x7fff) * 32) / 31);
aHalfS[ch] = (negA ? -valA : valA);
bHalfS[ch] = (negB ? -valB : valB);
}
bias = 63;
}
else
{
// final quantize step, such that max representable half float is 65504.0
// (which gets mapped to 0xffff)
for(int ch = 0; ch < 3; ch++)
{
aHalf[ch] = uint32_t(aHalf[ch] * 64) / 31;
bHalf[ch] = uint32_t(bHalf[ch] * 64) / 31;
}
bias = 15;
}
uint64_t colorbits = 0;
byte colorbit65 = 0;
// 10 bits for each value, RGB for A then RGB for B
colorbits |= uint64_t((aHalf[0] + bias) >> 6) << 0;
colorbits |= uint64_t((aHalf[1] + bias) >> 6) << 10;
colorbits |= uint64_t((aHalf[2] + bias) >> 6) << 20;
colorbits |= uint64_t((bHalf[0] + bias) >> 6) << 30;
colorbits |= uint64_t((bHalf[1] + bias) >> 6) << 40;
colorbits |= uint64_t((bHalf[2] + bias) >> 6) << 50; // overflows by 1 bit
colorbit65 = (bHalf[2] >> 15) & 0x1;
uint64_t block[2];
// first 64 bits are mode, and 59 of the color bits.
block[0] = mode << 0;
block[0] |= colorbits << 5;
// second 64-bit is the top bit of the colors bits, then the index bits
block[1] = (bc67indexbits << 1) | colorbit65;
memcpy(out, block, sizeof(block));
out += sizeof(block);
}
#define ROUND_7BIT(x) ((x) >> 1)
#define LO_BIT(x) ((x)&0x1)
if(bc7)
{
byte mode = 0x40;
// x1000000 = mode 6: no partition bits, no rotation bits, no index selection
// bit.
// 7 color bits, 7 alpha bits, 1 endpoint p-bit, 0 shared p-bits, 4 index bits,
// 0 secondary index bits
// color is stored R0, R1, G0, G1, B0, B1 because we only have one subset
uint64_t colorbits = 0;
colorbits |= uint64_t(ROUND_7BIT(a8[0])) << 0;
colorbits |= uint64_t(ROUND_7BIT(b8[0])) << 7;
colorbits |= uint64_t(ROUND_7BIT(a8[1])) << 14;
colorbits |= uint64_t(ROUND_7BIT(b8[1])) << 21;
colorbits |= uint64_t(ROUND_7BIT(a8[2])) << 28;
colorbits |= uint64_t(ROUND_7BIT(b8[2])) << 35;
uint64_t alphabits = 0;
alphabits |= uint64_t(ROUND_7BIT(a8[3])) << 0;
alphabits |= uint64_t(ROUND_7BIT(b8[3])) << 7;
byte endpointA = 0;
byte endpointB = 0;
// take a vote, if more than two of the original values have the low bit set,
// set
// the endpoint. The tie-break is towards zero because we're wanting *more* than
// two (so exactly two means 0)
if(LO_BIT(a8[0]) + LO_BIT(a8[1]) + LO_BIT(a8[2]) + LO_BIT(a8[3]) > 2)
endpointA = 1;
if(LO_BIT(b8[0]) + LO_BIT(b8[1]) + LO_BIT(b8[2]) + LO_BIT(b8[3]) > 2)
endpointB = 1;
uint64_t block[2];
// first 64 bits are mode, color, alpha, and endpoint A
block[0] = mode << 0;
block[0] |= colorbits << 7;
block[0] |= alphabits << (7 + 42);
block[0] |= uint64_t(endpointA & 0x1) << (7 + 42 + 14);
// second 64-bit is endpoint B, then the index bits
block[1] = (bc67indexbits << 1) | endpointB;
memcpy(out, block, sizeof(block));
out += sizeof(block);
}
}
}
srcPixels += floatdata.slicePitch / sizeof(Vec4f);
}
}
}
}
}; // namespace TextureZoo
File diff suppressed because it is too large Load Diff
+1
View File
@@ -3,3 +3,4 @@ from .capture import *
from .runner import *
from .analyse import *
from .testcase import *
from .shared.Texture_Zoo import *
+17
View File
@@ -341,6 +341,23 @@ def vulkan_register():
rd.UpdateVulkanLayerRegistration(True)
def launch_remote_server():
# Fork the interpreter to run the test, in case it crashes we can catch it.
# We can re-run with the same parameters
args = sys.argv.copy()
args.insert(0, sys.executable)
# Add parameter to run the remote server itself
args.append('--internal_remote_server')
subprocess.Popen(args)
return
def become_remote_server():
rd.BecomeRemoteServer('localhost', None, None)
def internal_run_test(test_name):
testcases = get_tests()
+479
View File
@@ -0,0 +1,479 @@
import renderdoc as rd
import rdtest
from typing import List, Tuple
import time
import os
# Not a real test, re-used by API-specific tests
class Texture_Zoo():
def __init__(self):
self.proxied = False
self.fake_msaa = False
self.textures = {}
self.filename = ''
self.textures = {}
self.controller: rd.ReplayController
self.controller = None
self.opengl_mode = False
def sub(self, mip: int, slice: int, sample: int):
if self.fake_msaa:
return rd.Subresource(mip, slice * 2 + sample, 0)
else:
return rd.Subresource(mip, slice, sample)
def pick(self, tex: rd.ResourceId, x: int, y: int, sub: rd.Subresource, typeCast: rd.CompType):
if self.opengl_mode:
y = max(1, self.textures[tex].height >> sub.mip) - 1 - y
return self.controller.PickPixel(tex, x, y, sub, typeCast)
TEST_CAPTURE = 0
TEST_DDS = 1
TEST_PNG = 2
def check_test(self, fmt_name: str, name: str, test_mode: int):
pipe: rd.PipeState = self.controller.GetPipelineState()
image_view = (test_mode != Texture_Zoo.TEST_CAPTURE)
if image_view:
bound_res: rd.BoundResource = pipe.GetOutputTargets()[0]
else:
bound_res: rd.BoundResource = pipe.GetReadOnlyResources(rd.ShaderStage.Pixel)[0].resources[0]
texs = self.controller.GetTextures()
for t in texs:
self.textures[t.resourceId] = t
tex_id: rd.ResourceId = bound_res.resourceId
tex: rd.TextureDescription = self.textures[tex_id]
comp_type: rd.CompType = tex.format.compType
if bound_res.typeCast != rd.CompType.Typeless:
comp_type = bound_res.typeCast
# When not running proxied, save non-typecasted textures to disk
if not image_view and not self.proxied and (tex.format.compType == comp_type or
tex.format.type == rd.ResourceFormatType.D24S8 or
tex.format.type == rd.ResourceFormatType.D32S8):
save_data = rd.TextureSave()
save_data.resourceId = tex_id
save_data.destType = rd.FileType.DDS
save_data.sample.mapToArray = True
self.textures[self.filename] = tex
path = rdtest.get_tmp_path(self.filename + '.dds')
success: bool = self.controller.SaveTexture(save_data, path)
if not success:
try:
os.remove(path)
except Exception:
pass
save_data.destType = rd.FileType.PNG
save_data.slice.sliceIndex = 0
save_data.sample.sampleIndex = 0
path = path.replace('.dds', '.png')
if comp_type == rd.CompType.UInt:
save_data.comp.blackPoint = 0.0
save_data.comp.whitePoint = 255.0
elif comp_type == rd.CompType.SInt:
save_data.comp.blackPoint = -255.0
save_data.comp.whitePoint = 0.0
elif comp_type == rd.CompType.SNorm:
save_data.comp.blackPoint = -1.0
save_data.comp.whitePoint = 0.0
success: bool = self.controller.SaveTexture(save_data, path)
if not success:
try:
os.remove(path)
except Exception:
pass
value0 = []
comp_count = tex.format.compCount
# When viewing PNGs only compare the components that the original texture had
if test_mode == Texture_Zoo.TEST_PNG:
comp_count = self.textures[self.filename]
tex.msSamp = 0
tex.arraysize = 1
tex.depth = 1
self.fake_msaa = 'MSAA' in name
elif test_mode == Texture_Zoo.TEST_DDS:
tex.arraysize = self.textures[self.filename].arraysize
tex.msSamp = self.textures[self.filename].msSamp
self.fake_msaa = 'MSAA' in name
# HACK: We don't properly support BGRX, so just drop the alpha channel. We can't set this to compCount = 3
# internally because that's a 24-bit format with no padding...
if 'B8G8R8X8' in fmt_name:
comp_count = 3
# Completely ignore the alpha for BC1, our encoder doesn't pay attention to it
if tex.format.type == rd.ResourceFormatType.BC1:
comp_count = 3
# Calculate format-appropriate epsilon
eps_significand = 1.0
# Account for the sRGB curve by more generous epsilon
if comp_type == rd.CompType.UNormSRGB:
eps_significand = 2.5
# Similarly SNorm essentially loses a bit of accuracy due to us only using negative values
elif comp_type == rd.CompType.SNorm:
eps_significand = 2.0
if tex.format.type == rd.ResourceFormatType.R4G4B4A4 or tex.format.type == rd.ResourceFormatType.R4G4:
eps = (eps_significand / 15.0)
elif rd.ResourceFormatType.BC1 <= tex.format.type <= rd.ResourceFormatType.BC3:
eps = (eps_significand / 15.0) # 4-bit precision in some channels
elif tex.format.type == rd.ResourceFormatType.R5G5B5A1 or tex.format.type == rd.ResourceFormatType.R5G6B5:
eps = (eps_significand / 31.0)
elif tex.format.type == rd.ResourceFormatType.R11G11B10:
eps = (eps_significand / 31.0) # 5-bit mantissa in blue
elif tex.format.type == rd.ResourceFormatType.R9G9B9E5:
eps = (eps_significand / 63.0) # we have 9 bits of data, but might lose 2-3 due to shared exponent
elif tex.format.type == rd.ResourceFormatType.BC6 and tex.format.compType == rd.CompType.SNorm:
eps = (eps_significand / 63.0) # Lose a bit worth of precision for the signed version
elif rd.ResourceFormatType.BC4 <= tex.format.type <= rd.ResourceFormatType.BC7:
eps = (eps_significand / 127.0)
elif tex.format.compByteWidth == 1:
eps = (eps_significand / 255.0)
elif comp_type == rd.CompType.Depth and tex.format.compCount == 2:
eps = (eps_significand / 255.0) # stencil is only 8-bit
elif tex.format.type == rd.ResourceFormatType.R10G10B10A2:
eps = (eps_significand / 1023.0)
else:
# half-floats have 11-bit mantissa. This epsilon is tight enough that we can be sure
# any remaining errors are implementation inaccuracy and not our bug
eps = (eps_significand / 2047.0)
for mp in range(tex.mips):
for sl in range(max(tex.arraysize, max(1, tex.depth >> mp))):
z = 0
if tex.depth > 1:
z = sl
for sm in range(tex.msSamp):
for x in range(max(1, tex.width >> mp)):
for y in range(max(1, tex.height >> mp)):
picked: rd.PixelValue = self.pick(tex_id, x, y, self.sub(mp, sl, sm), comp_type)
# each 3D slice cycles the x. This only affects the primary diagonal
offs_x = (x + z) % max(1, tex.width >> mp)
# The diagonal inverts the colors
inverted = (offs_x != y)
# second slice adds a coarse checkerboard pattern of inversion
if tex.arraysize > 1 and sl == 1 and ((int(x / 2) % 2) != (int(y / 2) % 2)):
inverted = not inverted
if comp_type == rd.CompType.UInt or comp_type == rd.CompType.SInt:
expected = [10, 40, 70, 100]
if inverted:
expected = list(reversed(expected))
expected = [c + 10 * (sm + mp) for c in expected]
if comp_type == rd.CompType.SInt:
picked = picked.intValue
else:
picked = picked.uintValue
elif (tex.format.type == rd.ResourceFormatType.D16S8 or
tex.format.type == rd.ResourceFormatType.D24S8 or
tex.format.type == rd.ResourceFormatType.D32S8):
# depth/stencil is a bit special
expected = [0.1, 10, 100, 0.85]
if inverted:
expected = list(reversed(expected))
expected[0] += 0.075 * (sm + mp)
expected[1] += 10 * (sm + mp)
# Normalise stencil value
expected[1] = expected[1] / 255.0
picked = picked.floatValue
else:
expected = [0.1, 0.35, 0.6, 0.85]
if inverted:
expected = list(reversed(expected))
expected = [c + 0.075 * (sm + mp) for c in expected]
picked = picked.floatValue
# SNorm/SInt is negative
if comp_type == rd.CompType.SNorm or comp_type == rd.CompType.SInt:
expected = [-c for c in expected]
# BGRA textures have a swizzle applied
if tex.format.BGRAOrder():
expected[0:3] = reversed(expected[0:3])
# alpha channel in 10:10:10:2 has extremely low precision, and the ULP requirements mean
# we basically can't trust anything between 0 and 1 on float formats. Just round in that
# case as it still lets us differentiate between alpha 0.0-0.5 and 0.5-1.0
if tex.format.type == rd.ResourceFormatType.R10G10B10A2:
if comp_type == rd.CompType.UInt:
expected[3] = min(3, expected[3])
else:
expected[3] = round(expected[3]) * 1.0
picked[3] = round(picked[3]) * 1.0
# Handle 1-bit alpha
if tex.format.type == rd.ResourceFormatType.R5G5B5A1:
expected[3] = 1.0 if expected[3] >= 0.5 else 0.0
picked[3] = 1.0 if picked[3] >= 0.5 else 0.0
# A8 picked values come out in alpha, but we want to compare against the single channel
if tex.format.type == rd.ResourceFormatType.A8:
picked[0] = picked[3]
# Clamp to number of components in the texture
expected = expected[0:comp_count]
picked = picked[0:comp_count]
if mp == 0 and sl == 0 and sm == 0 and x == 0 and y == 0:
value0 = picked
# For SRGB textures picked values will come out as linear
def srgb2linear(f):
if f <= 0.04045:
return f / 12.92
else:
return ((f + 0.055) / 1.055) ** 2.4
if comp_type == rd.CompType.UNormSRGB:
expected[0:3] = [srgb2linear(x) for x in expected[0:3]]
if test_mode == Texture_Zoo.TEST_PNG:
orig_comp = self.textures[self.filename].format.compType
if orig_comp == rd.CompType.SNorm or orig_comp == rd.CompType.SInt:
expected = [1.0 - x for x in expected]
if not rdtest.value_compare(picked, expected, eps):
raise rdtest.TestFailureException(
"At ({},{}) of slice {}, mip {}, sample {} of {} {} got {}. Expected {}".format(
x, y, sl, mp, sm, name, fmt_name, picked, expected))
if not image_view:
output_tex = pipe.GetOutputTargets()[0].resourceId
# in the test captures pick the output texture, it should be identical to the
# (0,0) pixel in slice 0, mip 0, sample 0
view: rd.Viewport = pipe.GetViewport(0)
val: rd.PixelValue = self.pick(pipe.GetOutputTargets()[0].resourceId, int(view.x + view.width / 2),
int(view.y + view.height / 2), rd.Subresource(), rd.CompType.Typeless)
picked = val.floatValue
# A8 picked values come out in alpha, but we want to compare against the single channel
if tex.format.type == rd.ResourceFormatType.A8:
picked[0] = picked[3]
# Clamp to number of components in the texture
picked = picked[0:comp_count]
# Up-convert any non-float expected values to floats
value0 = [float(x) for x in value0]
# For depth/stencil images, one of either depth or stencil should match
if comp_type == rd.CompType.Depth and len(value0) == 2:
if picked[0] == 0.0:
value0[0] = 0.0
# normalise stencil value if it isn't already
if picked[1] > 1.0:
picked[1] /= 255.0
elif picked[0] > 1.0:
# un-normalised stencil being rendered in red, match against our stencil expectation
picked[0] /= 255.0
value0[0] = value0[1]
value0[1] = 0.0
else:
value0[1] = 0.0
if not rdtest.value_compare(picked, value0, eps):
raise rdtest.TestFailureException(
"In {} {} Top-left pixel as rendered is {}. Expected {}".format(name, fmt_name, picked, value0))
def check_capture_with_controller(self, proxy_api: str):
any_failed = False
if proxy_api != '':
rdtest.log.print('Running with {} local proxy'.format(proxy_api))
self.proxied = True
else:
rdtest.log.print('Running on direct replay')
self.proxied = False
for d in self.controller.GetDrawcalls():
# Check each region for the tests within
if d.flags & rd.DrawFlags.PushMarker:
name = ''
tests_run = 0
failed = False
# Iterate over drawcalls in this region
for sub in d.children:
sub: rd.DrawcallDescription
if sub.flags & rd.DrawFlags.SetMarker:
name = sub.name
# Check this draw
if sub.flags & rd.DrawFlags.Drawcall:
tests_run = tests_run + 1
try:
# Set this event as current
self.controller.SetFrameEvent(sub.eventId, True)
self.filename = (d.name + '@' + name).replace('->', '_')
self.check_test(d.name, name, Texture_Zoo.TEST_CAPTURE)
except rdtest.TestFailureException as ex:
failed = any_failed = True
rdtest.log.error(str(ex))
if not failed:
rdtest.log.success("All {} texture tests for {} are OK".format(tests_run, d.name))
if not any_failed:
if proxy_api != '':
rdtest.log.success(
'All textures are OK with {} as local proxy'.format(proxy_api))
else:
rdtest.log.success("All textures are OK on direct replay")
else:
raise rdtest.TestFailureException("Some tests were not as expected")
def check_capture(self, capture_filename: str, controller: rd.ReplayController):
self.controller = controller
self.opengl_mode = (self.controller.GetAPIProperties().pipelineType == rd.GraphicsAPI.OpenGL)
failed = False
try:
# First check with the local controller
self.check_capture_with_controller('')
except rdtest.TestFailureException as ex:
rdtest.log.error(str(ex))
failed = True
# Now shut it down
self.controller.Shutdown()
self.controller = None
# Launch a remote server
rdtest.launch_remote_server()
# Wait for it to start
time.sleep(0.5)
ret: Tuple[rd.ReplayStatus, rd.RemoteServer] = rd.CreateRemoteServerConnection('localhost')
status, remote = ret
proxies = remote.LocalProxies()
try:
# Try D3D11 and GL as proxies, D3D12/Vulkan technically don't have proxying implemented even though they
# will be listed in proxies
for api in ['D3D11', 'OpenGL']:
if api not in proxies:
continue
try:
ret: Tuple[rd.ReplayStatus, rd.ReplayController] = remote.OpenCapture(proxies.index(api),
capture_filename,
rd.ReplayOptions(), None)
status, self.controller = ret
# Now check with the proxy
self.check_capture_with_controller(api)
except ValueError:
continue
except rdtest.TestFailureException as ex:
rdtest.log.error(str(ex))
failed = True
finally:
self.controller.Shutdown()
self.controller = None
finally:
remote.ShutdownServerAndConnection()
# Now iterate over all the temp images saved out, load them as captures, and check the texture.
dir_path = rdtest.get_tmp_path('')
was_opengl = self.opengl_mode
# We iterate in filename order, so that dds files get opened before png files.
for file in os.scandir(dir_path):
if '.dds' not in file.name and '.png' not in file.name:
continue
cap = rd.OpenCaptureFile()
status = cap.OpenFile(file.path, 'rdc', None)
if status != rd.ReplayStatus.Succeeded:
rdtest.log.error("Couldn't open {}".format(file.name))
failed = True
continue
ret: Tuple[rd.ReplayStatus, rd.ReplayController] = cap.OpenCapture(rd.ReplayOptions(), None)
status, self.controller = ret
if status != rd.ReplayStatus.Succeeded:
rdtest.log.error("Couldn't open {}".format(file.name))
failed = True
continue
self.filename = file.name.replace('.dds', '').replace('.png', '')
[a, b] = file.name.replace('.dds', ' (DDS)').replace('.png', ' (PNG)').split('@')
self.controller.SetFrameEvent(self.controller.GetDrawcalls()[0].eventId, True)
try:
self.opengl_mode = False
fmt: rd.ResourceFormat = self.controller.GetTextures()[0].format
is_compressed = (rd.ResourceFormatType.BC1 <= fmt.type <= rd.ResourceFormatType.BC7 or
fmt.type == rd.ResourceFormatType.EAC or fmt.type == rd.ResourceFormatType.ETC2 or
fmt.type == rd.ResourceFormatType.ASTC or fmt.type == rd.ResourceFormatType.PVRTC)
# OpenGL saves all non-compressed images to disk with a flip, since that's the expected order for
# most formats. The effect of this is that we should apply the opengl_mode workaround for all files
# *except* compressed textures
if was_opengl and not is_compressed:
self.opengl_mode = True
self.check_test(a, b, Texture_Zoo.TEST_DDS if '.dds' in file.name else Texture_Zoo.TEST_PNG)
rdtest.log.success("{} loaded with the correct data".format(file.name))
except rdtest.TestFailureException as ex:
rdtest.log.error(str(ex))
failed = True
self.controller.Shutdown()
self.controller = None
if failed:
raise rdtest.TestFailureException("Some tests were not as expected")
+2 -1
View File
@@ -328,7 +328,8 @@ class TestCase:
self.check_capture()
self.controller.Shutdown()
if self.controller is not None:
self.controller.Shutdown()
def invoketest(self, debugMode):
self.run()
+4 -3
View File
@@ -250,9 +250,10 @@ FLT_EPSILON = 2.0*1.19209290E-07
def value_compare(ref, data, eps=FLT_EPSILON):
if type(ref) == float:
if type(data) != float:
return False
if type(ref) == float or type(data) == float:
# if the types are different this is probably 0.0 == 0 or something. Just compare straight by casting to floats
if type(data) != type(data):
return float(data) == float(ref)
# Special handling for NaNs - NaNs are always equal to NaNs, but NaN is never equal to any other value
if math.isnan(ref) and math.isnan(data):
+4
View File
@@ -36,6 +36,8 @@ parser.add_argument('--debugger',
parser.add_argument('--internal_run_test', help=argparse.SUPPRESS, type=str, required=False)
# Internal command, when we re-run as admin to register vulkan layer
parser.add_argument('--internal_vulkan_register', help=argparse.SUPPRESS, action="store_true", required=False)
# Internal command, when we re-run as a remote server
parser.add_argument('--internal_remote_server', help=argparse.SUPPRESS, action="store_true", required=False)
args = parser.parse_args()
if args.renderdoc is not None:
@@ -110,6 +112,8 @@ if args.debugger:
if args.internal_vulkan_register:
rdtest.vulkan_register()
elif args.internal_remote_server:
rdtest.become_remote_server()
elif args.internal_run_test is not None:
rdtest.internal_run_test(args.internal_run_test)
else:
@@ -0,0 +1,19 @@
import renderdoc as rd
import rdtest
from typing import List, Tuple
import time
import os
class D3D11_Texture_Zoo(rdtest.TestCase):
slow_test = True
demos_test_name = 'D3D11_Texture_Zoo'
def __init__(self):
rdtest.TestCase.__init__(self)
self.zoo_helper = rdtest.Texture_Zoo()
def check_capture(self):
# This takes ownership of the controller and shuts it down when it's finished
self.zoo_helper.check_capture(self.capture_filename, self.controller)
self.controller = None
@@ -0,0 +1,19 @@
import renderdoc as rd
import rdtest
from typing import List, Tuple
import time
import os
class D3D12_Texture_Zoo(rdtest.TestCase):
slow_test = True
demos_test_name = 'D3D12_Texture_Zoo'
def __init__(self):
rdtest.TestCase.__init__(self)
self.zoo_helper = rdtest.Texture_Zoo()
def check_capture(self):
# This takes ownership of the controller and shuts it down when it's finished
self.zoo_helper.check_capture(self.capture_filename, self.controller)
self.controller = None
+19
View File
@@ -0,0 +1,19 @@
import renderdoc as rd
import rdtest
from typing import List, Tuple
import time
import os
class GL_Texture_Zoo(rdtest.TestCase):
slow_test = True
demos_test_name = 'GL_Texture_Zoo'
def __init__(self):
rdtest.TestCase.__init__(self)
self.zoo_helper = rdtest.Texture_Zoo()
def check_capture(self):
# This takes ownership of the controller and shuts it down when it's finished
self.zoo_helper.check_capture(self.capture_filename, self.controller)
self.controller = None
+19
View File
@@ -0,0 +1,19 @@
import renderdoc as rd
import rdtest
from typing import List, Tuple
import time
import os
class VK_Texture_Zoo(rdtest.TestCase):
slow_test = True
demos_test_name = 'VK_Texture_Zoo'
def __init__(self):
rdtest.TestCase.__init__(self)
self.zoo_helper = rdtest.Texture_Zoo()
def check_capture(self):
# This takes ownership of the controller and shuts it down when it's finished
self.zoo_helper.check_capture(self.capture_filename, self.controller)
self.controller = None