/****************************************************************************** * The MIT License (MIT) * * Copyright (c) 2019-2025 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 #include "gl_test.h" RD_TEST(GL_Pixel_History, OpenGLGraphicsTest) { static constexpr const char *Description = "Tests pixel history"; std::string common = R"EOSHADER( #version 460 core )EOSHADER"; const std::string vertex = R"EOSHADER( out gl_PerVertex { vec4 gl_Position; float gl_PointSize; }; layout(location = 0) in vec3 Position; layout(location = 1) in vec4 Color; layout(location = 0) INTERP out COL_TYPE outCol; void main() { gl_Position = vec4(Position.xyz, 1); outCol = ProcessColor(Color); } )EOSHADER"; const std::string pixel = R"EOSHADER( layout(location = 0) INTERP in COL_TYPE inCol; #if COLOR == 1 layout(location = 0, index = 0) out COL_TYPE Color; #endif void main() { if (gl_FragCoord.x < DISCARD_X+1 && gl_FragCoord.x > DISCARD_X) discard; if (inCol.x > 10000 && inCol.y > 10000 && inCol.z > 10000) discard; #if COLOR == 1 Color = inCol + COL_TYPE(0, 0, 0, ProcessColor(vec4(ALPHA_ADD)).x); #endif } )EOSHADER"; std::string mspixel = R"EOSHADER( #if COLOR == 1 layout(location = 0, index = 0) out COL_TYPE Color; #endif void main() { #if COLOR == 1 vec4 col; if (gl_SampleID == 0) col = vec4(1, 0, 0, 1+ALPHA_ADD); else if (gl_SampleID == 1) col = vec4(0, 0, 1, 1+ALPHA_ADD); else if (gl_SampleID == 2) col = vec4(0, 1, 1, 1+ALPHA_ADD); else if (gl_SampleID == 3) col = vec4(1, 1, 1, 1+ALPHA_ADD); Color = ProcessColor(col); #endif } )EOSHADER"; std::string comp = R"EOSHADER( layout(binding = 0) writeonly uniform IMAGE_TYPE Output; layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; void main() { vec4 data = vec4(3, 3, 3, 9); for(int x=0; x < 10; x++) for(int y=0; y < 10; y++) imageStore(Output, ivec3(STORE_X+x, STORE_Y+y, STORE_Z) STORE_SAMPLE, ProcessColor(data)); } )EOSHADER"; void Prepare(int argc, char **argv) { OpenGLGraphicsTest::Prepare(argc, argv); if(!Avail.empty()) return; if(!GL_ARB_clip_control) Avail = "Require GL_ARB_clip_control"; if(!GL_EXT_depth_bounds_test) Avail = "Require GL_EXT_depth_bounds_test"; } struct GLStateSetup { // only one will be set GLuint program; GLuint pipeline; bool depthTest; bool depthWrites; GLenum depthFunc; bool stencilTest; bool depthBounds; GLenum stencilFunc; uint32_t stencilMask; GLenum cullMode; uint32_t stencilRef; GLuint sampleMask; GLuint colorMask; void set() const { glSampleMaski(0, sampleMask); glUseProgram(program); glBindProgramPipeline(pipeline); if(depthTest) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST); if(depthBounds) glEnable(GL_DEPTH_BOUNDS_TEST_EXT); else glDisable(GL_DEPTH_BOUNDS_TEST_EXT); glDepthMask(depthWrites ? GL_TRUE : GL_FALSE); glDepthFunc(depthFunc); if(stencilTest) glEnable(GL_STENCIL_TEST); else glDisable(GL_STENCIL_TEST); glStencilFuncSeparate(GL_FRONT, stencilFunc, stencilRef, stencilMask); glCullFace(cullMode); glColorMask(colorMask & 0x1, colorMask & 0x2, colorMask & 0x4, colorMask & 0x8); } }; struct GLTestBatch { std::string name; bool uint = false; uint32_t mip = 0; GLenum colFmt; GLuint colImg2D; GLuint framebuffer; GLenum depthFormat; GLuint compProg; uint32_t width, height; GLStateSetup depthWriteState; GLStateSetup scissorState; GLStateSetup stencilRefState; GLStateSetup stencilMaskState; GLStateSetup depthEqualState; GLStateSetup depthState; GLStateSetup stencilWriteState; GLStateSetup backgroundState; GLStateSetup noPsState; GLStateSetup basicState; GLStateSetup colorMaskState; GLStateSetup cullFrontState; GLStateSetup depthBoundsState; GLStateSetup sampleColourState; }; std::vector batches; void BuildTestBatch(const std::string &name, bool pipeline, uint32_t sampleCount, GLenum colourFormat, GLenum depthStencilFormat, int targetMip = -1, int targetSlice = -1, int targetDepthSlice = -1) { batches.push_back({}); GLTestBatch &batch = batches.back(); batch.name = name; batch.width = screenWidth; batch.height = screenHeight; uint32_t arraySize = targetSlice >= 0 ? 5 : 1; uint32_t mips = targetMip >= 0 ? 4 : 1; uint32_t depth = targetDepthSlice >= 0 ? 14 : 1; uint32_t mip = (uint32_t)std::max(0, targetMip); uint32_t slice = (uint32_t)std::max(0, targetSlice); if(depth > 1) slice = (uint32_t)targetDepthSlice; batch.mip = mip; std::string defines; defines += "#define COLOR " + std::string(colourFormat == GL_NONE ? "0" : "1") + "\n"; std::string imgprefix; if(colourFormat == GL_RGBA8UI || colourFormat == GL_RGBA16UI || colourFormat == GL_RGBA32UI) { batch.uint = true; imgprefix = "u"; defines += R"( #define COL_TYPE uvec4 #define ALPHA_ADD 3 #define INTERP flat uvec4 ProcessColor(vec4 col) { uvec4 ret = uvec4(16*col); if(col.x < 0.0f) ret.x = 0xfffffff0; if(col.y < 0.0f) ret.y = 0xfffffff0; if(col.z < 0.0f) ret.z = 0xfffffff0; return ret; } )"; } else { defines += R"( #define COL_TYPE vec4 #define ALPHA_ADD 1.75 #define INTERP vec4 ProcessColor(vec4 col) { vec4 ret = col; // this obviously won't overflow F32 but it will overflow anything 16-bit and under if(col.x < 0.0f) ret.x = 100000.0f; if(col.y < 0.0f) ret.y = 100000.0f; if(col.z < 0.0f) ret.z = 100000.0f; return ret; } )"; } defines += "#define DISCARD_X " + std::to_string(150 >> mip) + "\n"; defines += "#define IMAGE_TYPE " + std::string(depth > 1 ? imgprefix + "image3D" : sampleCount != 1 ? imgprefix + "image2DMSArray" : imgprefix + "image2DArray") + "\n"; defines += "#define STORE_X " + std::to_string(220 >> mip) + "\n"; defines += "#define STORE_Y " + std::to_string(210 >> mip) + "\n"; defines += "#define STORE_Z " + std::to_string(slice) + "\n"; defines += "#define STORE_SAMPLE " + std::string(sampleCount != 1 ? ", 0" : "") + "\n"; defines += "\n\n"; batch.framebuffer = MakeFBO(); glBindFramebuffer(GL_FRAMEBUFFER, batch.framebuffer); if(colourFormat != GL_NONE) { GLuint colTex = MakeTexture(); if(sampleCount > 1) { if(arraySize > 1) { glBindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, colTex); glTextureStorage3DMultisample(colTex, sampleCount, colourFormat, batch.width, batch.height, arraySize, GL_TRUE); } else { glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colTex); glTextureStorage2DMultisample(colTex, sampleCount, colourFormat, batch.width, batch.height, GL_TRUE); } } else if(depth > 1) { glBindTexture(GL_TEXTURE_3D, colTex); glTextureStorage3D(colTex, mips, colourFormat, batch.width, batch.height, depth); } else if(arraySize > 1) { glBindTexture(GL_TEXTURE_2D_ARRAY, colTex); glTextureStorage3D(colTex, mips, colourFormat, batch.width, batch.height, arraySize); batch.colImg2D = colTex; } else { glBindTexture(GL_TEXTURE_2D, colTex); glTextureStorage2D(colTex, mips, colourFormat, batch.width, batch.height); batch.colImg2D = colTex; } if(arraySize > 1 || depth > 1) glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, colTex, mip, slice); else glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, colTex, mip); batch.compProg = MakeProgram(common + defines + comp); batch.colFmt = colourFormat; } batch.depthFormat = depthStencilFormat; if(depthStencilFormat != GL_NONE) { GLuint depthTex = MakeTexture(); if(sampleCount > 1) { if(arraySize > 1) { glBindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, depthTex); glTextureStorage3DMultisample(depthTex, sampleCount, depthStencilFormat, batch.width, batch.height, arraySize, GL_TRUE); } else { glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, depthTex); glTextureStorage2DMultisample(depthTex, sampleCount, depthStencilFormat, batch.width, batch.height, GL_TRUE); } } else if(depth > 1 || arraySize > 1) { // can't create 2D views of 3D with depth images, so we just use a normal 2D array image glBindTexture(GL_TEXTURE_2D_ARRAY, depthTex); glTextureStorage3D(depthTex, mips, depthStencilFormat, batch.width, batch.height, std::max(depth, arraySize)); } else { glBindTexture(GL_TEXTURE_2D, depthTex); glTextureStorage2D(depthTex, mips, depthStencilFormat, batch.width, batch.height); } GLenum att = GL_DEPTH_STENCIL_ATTACHMENT; if(depthStencilFormat == GL_DEPTH_COMPONENT16 || depthStencilFormat == GL_DEPTH_COMPONENT32F) att = GL_DEPTH_ATTACHMENT; if(arraySize > 1 || depth > 1) glFramebufferTextureLayer(GL_FRAMEBUFFER, att, depthTex, mip, slice); else glFramebufferTexture(GL_FRAMEBUFFER, att, depthTex, mip); } batch.width >>= mip; batch.height >>= mip; GLStateSetup stateInfo = {}; if(pipeline) { stateInfo.pipeline = MakePipeline(); glUseProgramStages(stateInfo.pipeline, GL_VERTEX_SHADER_BIT, MakePipelineProgram(GL_VERTEX_SHADER, common + defines + vertex)); glUseProgramStages(stateInfo.pipeline, GL_FRAGMENT_SHADER_BIT, MakePipelineProgram(GL_FRAGMENT_SHADER, common + defines + pixel)); } else { stateInfo.program = MakeProgram(common + defines + vertex, common + defines + pixel); } glEnable(GL_CULL_FACE); glFrontFace(GL_CW); glEnable(GL_MULTISAMPLE); glEnable(GL_SAMPLE_MASK); glEnable(GL_SCISSOR_TEST); glDisable(GL_DEPTH_CLAMP); glStencilOpSeparate(GL_FRONT_AND_BACK, GL_KEEP, GL_KEEP, GL_REPLACE); // this isn't a full test because almost all GL applications will be -1 to 1 but we hope/assume // that our code isn't sensitive to that and just treats depth as whatever it comes out as. // Using zero to one means all our testing code can be unified glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); stateInfo.depthTest = true; stateInfo.depthWrites = true; stateInfo.depthFunc = GL_ALWAYS; stateInfo.stencilTest = false; stateInfo.stencilFunc = GL_ALWAYS; stateInfo.stencilMask = 0xff; stateInfo.cullMode = GL_BACK; stateInfo.stencilRef = 0x55; stateInfo.sampleMask = ~0U; stateInfo.colorMask = 0xf; stateInfo.depthBounds = false; batch.depthWriteState = stateInfo; { GLStateSetup depthEqualStateInfo = stateInfo; depthEqualStateInfo.depthWrites = GL_FALSE; depthEqualStateInfo.depthFunc = GL_EQUAL; batch.depthEqualState = depthEqualStateInfo; } { GLStateSetup scissorStencilStates = stateInfo; scissorStencilStates.depthWrites = GL_FALSE; scissorStencilStates.depthTest = GL_FALSE; batch.scissorState = scissorStencilStates; batch.stencilRefState = scissorStencilStates; batch.stencilRefState.stencilRef = 0x67; scissorStencilStates.stencilMask = 0; batch.stencilMaskState = scissorStencilStates; } stateInfo.depthFunc = GL_LEQUAL; stateInfo.depthBounds = true; batch.depthState = stateInfo; stateInfo.depthBounds = false; stateInfo.stencilTest = GL_TRUE; batch.stencilWriteState = stateInfo; stateInfo.stencilTest = GL_FALSE; batch.backgroundState = stateInfo; stateInfo.stencilTest = GL_TRUE; batch.noPsState = stateInfo; batch.noPsState.stencilRef = 0x33; if(pipeline) { batch.noPsState.pipeline = MakePipeline(); glUseProgramStages(batch.noPsState.pipeline, GL_VERTEX_SHADER_BIT, MakePipelineProgram(GL_VERTEX_SHADER, common + defines + vertex)); } else { batch.noPsState.program = MakeProgram(common + defines + vertex, ""); } stateInfo.stencilFunc = GL_GREATER; batch.basicState = stateInfo; stateInfo.stencilTest = GL_FALSE; { GLStateSetup maskStateInfo = stateInfo; maskStateInfo.colorMask = 0x3; batch.colorMaskState = maskStateInfo; } stateInfo.cullMode = GL_FRONT; batch.cullFrontState = stateInfo; stateInfo.cullMode = GL_BACK; stateInfo.depthBounds = true; batch.depthBoundsState = stateInfo; stateInfo.depthBounds = false; if(pipeline) { stateInfo.pipeline = MakePipeline(); glUseProgramStages(stateInfo.pipeline, GL_VERTEX_SHADER_BIT, MakePipelineProgram(GL_VERTEX_SHADER, common + defines + vertex)); glUseProgramStages(stateInfo.pipeline, GL_FRAGMENT_SHADER_BIT, MakePipelineProgram(GL_FRAGMENT_SHADER, common + defines + mspixel)); } else { stateInfo.program = MakeProgram(common + defines + vertex, common + defines + mspixel); } stateInfo.depthWrites = GL_TRUE; batch.sampleColourState = stateInfo; if(sampleCount == 4) batch.sampleColourState.sampleMask = 0x7; } void RunDraw(const PixelHistory::draw &draw, uint32_t numInstances = 1) { glDrawArraysInstanced(GL_TRIANGLES, draw.first, draw.count, numInstances); } void RunBatch(const GLTestBatch &b) { float factor = float(b.width) / float(screenWidth); // use ceil instead of floor on y to better match the other APIs with top left origin glViewportIndexedf(0, floorf(10.0f * factor), ceilf(10.0f * factor), (float)b.width - (20.0f * factor), (float)b.height - (20.0f * factor)); glScissor(0, 0, b.width, b.height); // draw the setup triangles pushMarker("Setup"); { setMarker("Depth Write"); b.depthWriteState.set(); RunDraw(PixelHistory::DepthWrite); setMarker("Depth Equal Setup"); RunDraw(PixelHistory::DepthEqualSetup); setMarker("Unbound Shader"); b.noPsState.set(); RunDraw(PixelHistory::UnboundPS); setMarker("Stencil Write"); b.stencilWriteState.set(); RunDraw(PixelHistory::StencilWrite); setMarker("Background"); b.backgroundState.set(); RunDraw(PixelHistory::Background); setMarker("Cull Front"); b.cullFrontState.set(); RunDraw(PixelHistory::CullFront); setMarker("Depth Bounds Prep"); b.depthBoundsState.set(); glDepthBoundsEXT(0.0f, 1.0f); RunDraw(PixelHistory::DepthBoundsPrep); setMarker("Depth Bounds Clip"); glDepthBoundsEXT(0.4f, 0.6f); RunDraw(PixelHistory::DepthBoundsClip); } popMarker(); pushMarker("Stress Test"); { pushMarker("Lots of Drawcalls"); { setMarker("300 Draws"); b.depthWriteState.set(); for(int d = 0; d < 300; ++d) RunDraw(PixelHistory::Draws300); } popMarker(); setMarker("300 Instances"); RunDraw(PixelHistory::Instances300, 300); } popMarker(); setMarker("Simple Test"); b.basicState.set(); RunDraw(PixelHistory::MainTest); b.scissorState.set(); setMarker("Scissor Fail"); // set up scissors anchored top left as we expect in the tests glScissor(95 >> b.mip, (55 >> b.mip) - (8 >> b.mip), 8 >> b.mip, 8 >> b.mip); RunDraw(PixelHistory::ScissorFail); setMarker("Scissor Pass"); glScissor(95 >> b.mip, (55 >> b.mip) - (8 >> b.mip), 20 >> b.mip, 8 >> b.mip); RunDraw(PixelHistory::ScissorPass); glScissor(0, 0, b.width, b.height); setMarker("Stencil Ref"); b.stencilRefState.set(); RunDraw(PixelHistory::StencilRef); setMarker("Stencil Mask"); b.stencilMaskState.set(); RunDraw(PixelHistory::StencilMask); setMarker("Depth Test"); b.depthState.set(); glDepthBoundsEXT(0.15f, 1.0f); RunDraw(PixelHistory::DepthTest); setMarker("Sample Colouring"); b.sampleColourState.set(); RunDraw(PixelHistory::SampleColour); setMarker("Depth Equal Fail"); b.depthEqualState.set(); RunDraw(PixelHistory::DepthEqualFail); setMarker("Depth Equal Pass"); b.depthEqualState.set(); if(b.depthFormat == GL_DEPTH24_STENCIL8) RunDraw(PixelHistory::DepthEqualPass24); else if(b.depthFormat == GL_DEPTH_COMPONENT16) RunDraw(PixelHistory::DepthEqualPass16); else RunDraw(PixelHistory::DepthEqualPass32); setMarker("Colour Masked"); b.colorMaskState.set(); RunDraw(PixelHistory::ColourMask); setMarker("Overflowing"); b.backgroundState.set(); RunDraw(PixelHistory::OverflowingDraw); setMarker("Per-Fragment discarding"); b.backgroundState.set(); RunDraw(PixelHistory::PerFragDiscard); setMarker("No Output Shader"); b.noPsState.set(); RunDraw(PixelHistory::UnboundPS); } int main() { // initialise, create window, create context, etc if(!Init()) return 3; PixelHistory::init(); GLuint vao = MakeVAO(); glBindVertexArray(vao); GLuint vb = MakeBuffer(); glBindBuffer(GL_ARRAY_BUFFER, vb); glBufferStorage(GL_ARRAY_BUFFER, sizeof(DefaultA2V) * PixelHistory::vb.size(), PixelHistory::vb.data(), 0); ConfigureDefaultVAO(); GLuint bbBlitSource = 0; BuildTestBatch("Basic", false, 1, GL_RGBA8, GL_DEPTH32F_STENCIL8); bbBlitSource = batches.back().colImg2D; BuildTestBatch("Basic Pipeline", true, 1, GL_RGBA8, GL_DEPTH32F_STENCIL8); BuildTestBatch("MSAA", false, 4, GL_RGBA8, GL_DEPTH32F_STENCIL8); BuildTestBatch("Colour Only", false, 1, GL_RGBA8, GL_NONE); BuildTestBatch("Depth Only", false, 1, GL_NONE, GL_DEPTH32F_STENCIL8); BuildTestBatch("Mip & Slice", false, 1, GL_RGBA8, GL_DEPTH32F_STENCIL8, 2, 3); BuildTestBatch("Slice MSAA", false, 4, GL_RGBA8, GL_DEPTH32F_STENCIL8, -1, 3); // can't test 3D textures with depth as 3D depth is not supported and can't mix 3D with 2D array as in other APIs BuildTestBatch("3D texture", false, 1, GL_RGBA8, GL_NONE, -1, -1, 8); BuildTestBatch("D24S8", false, 1, GL_RGBA8, GL_DEPTH24_STENCIL8); BuildTestBatch("D16", false, 1, GL_RGBA8, GL_DEPTH_COMPONENT16); BuildTestBatch("D32", false, 1, GL_RGBA8, GL_DEPTH_COMPONENT32F); BuildTestBatch("F16 UNORM", false, 1, GL_RGBA16, GL_DEPTH32F_STENCIL8); BuildTestBatch("F16 FLOAT", false, 1, GL_RGBA16F, GL_DEPTH32F_STENCIL8); BuildTestBatch("F32 FLOAT", false, 1, GL_RGBA32F, GL_DEPTH32F_STENCIL8); BuildTestBatch("8-bit uint", false, 1, GL_RGBA8UI, GL_DEPTH32F_STENCIL8); BuildTestBatch("16-bit uint", false, 1, GL_RGBA16UI, GL_DEPTH32F_STENCIL8); BuildTestBatch("32-bit uint", false, 1, GL_RGBA32UI, GL_DEPTH32F_STENCIL8); while(Running()) { for(const GLTestBatch &b : batches) { { pushMarker("Batch: " + b.name); { setMarker("Begin RenderPass"); glBindFramebuffer(GL_FRAMEBUFFER, b.framebuffer); glScissor(0, 0, b.width, b.height); GLfloat clearColor[4] = {0.2f, 0.2f, 0.2f, 1.0f}; GLuint clearColorUInt[4] = {80, 80, 80, 16}; if(b.uint) glClearBufferuiv(GL_COLOR, 0, clearColorUInt); else glClearBufferfv(GL_COLOR, 0, clearColor); glClearBufferfi(GL_DEPTH_STENCIL, 0, 1.0f, 0); RunBatch(b); if(b.colImg2D) { setMarker("Compute write"); glBindFramebuffer(GL_FRAMEBUFFER, 0); glUseProgram(b.compProg); glBindImageTexture(0, b.colImg2D, b.mip, GL_TRUE, 0, GL_READ_WRITE, b.colFmt); glDispatchCompute(1, 1, 1); } } popMarker(); } } if(bbBlitSource) { blitToSwap(bbBlitSource); } Present(); } return 0; } }; REGISTER_TEST();