diff --git a/tests/display/conftest.py b/tests/display/conftest.py index 09684fab84..474ab5ee0b 100644 --- a/tests/display/conftest.py +++ b/tests/display/conftest.py @@ -43,3 +43,28 @@ def window(graphics_pipe, graphics_engine): if win is not None: graphics_engine.remove_window(win) + + +@pytest.fixture(scope='module') +def gsg(graphics_pipe, graphics_engine): + "Returns a windowless GSG that can be used for offscreen rendering." + from panda3d.core import GraphicsPipe, FrameBufferProperties, WindowProperties + + fbprops = FrameBufferProperties() + fbprops.force_hardware = True + + buffer = graphics_engine.make_output( + graphics_pipe, + 'buffer', + 0, + fbprops, + WindowProperties.size(32, 32), + GraphicsPipe.BF_refuse_window + ) + graphics_engine.open_windows() + + assert buffer is not None + yield buffer.gsg + + if buffer is not None: + graphics_engine.remove_window(buffer) diff --git a/tests/display/test_glsl_shader.py b/tests/display/test_glsl_shader.py new file mode 100644 index 0000000000..f4ff2d7ea0 --- /dev/null +++ b/tests/display/test_glsl_shader.py @@ -0,0 +1,277 @@ +from panda3d import core +import pytest +from _pytest.outcomes import Failed + + +# This is the template for the compute shader that is used by run_glsl_test. +# It defines an assert() macro that writes failures to a buffer, indexed by +# line number. +# The reset() function serves to prevent the _triggered variable from being +# optimized out in the case that the assertions are being optimized out. +GLSL_COMPUTE_TEMPLATE = """#version {version} + +layout(local_size_x = 1, local_size_y = 1) in; + +{preamble} + +layout(r8ui) uniform writeonly uimageBuffer _triggered; + +void _reset() {{ + imageStore(_triggered, 0, uvec4(0, 0, 0, 0)); +}} + +void _assert(bool cond, int line) {{ + if (!cond) {{ + imageStore(_triggered, line, uvec4(1)); + }} +}} + +#define assert(cond) _assert(cond, __LINE__) + +void main() {{ + _reset(); +{body} +}} +""" + + +def run_glsl_test(gsg, body, preamble="", inputs={}, version=430): + """ Runs a GLSL test on the given GSG. The given body is executed in the + main function and should call assert(). The preamble should contain all + of the shader inputs. """ + + if not gsg.supports_compute_shaders or not gsg.supports_glsl: + pytest.skip("compute shaders not supported") + + __tracebackhide__ = True + + preamble = preamble.strip() + body = body.rstrip().lstrip('\n') + code = GLSL_COMPUTE_TEMPLATE.format(version=version, preamble=preamble, body=body) + line_offset = code[:code.find(body)].count('\n') + 1 + shader = core.Shader.make_compute(core.Shader.SL_GLSL, code) + assert shader, code + + # Create a buffer to hold the results of the assertion. We use one byte + # per line of shader code, so we can show which lines triggered. + result = core.Texture("") + result.set_clear_color((0, 0, 0, 0)) + result.setup_buffer_texture(code.count('\n'), core.Texture.T_unsigned_byte, + core.Texture.F_r8i, core.GeomEnums.UH_static) + + # Build up the shader inputs + attrib = core.ShaderAttrib.make(shader) + for name, value in inputs.items(): + attrib = attrib.set_shader_input(name, value) + attrib = attrib.set_shader_input('_triggered', result) + + # Run the compute shader. + engine = core.GraphicsEngine.get_global_ptr() + try: + engine.dispatch_compute((1, 1, 1), attrib, gsg) + except AssertionError as exc: + assert False, "Error executing compute shader:\n" + code + + # Download the texture to check whether the assertion triggered. + assert engine.extract_texture_data(result, gsg) + triggered = result.get_ram_image() + if any(triggered): + count = len(triggered) - triggered.count(0) + lines = body.split('\n') + formatted = '' + for i, line in enumerate(lines): + if triggered[i + line_offset]: + formatted += '=> ' + line + '\n' + else: + formatted += ' ' + line + '\n' + pytest.fail("{0} GLSL assertions triggered:\n{1}".format(count, formatted)) + + +def test_glsl_test(gsg): + "Test to make sure that the GLSL tests work correctly." + + run_glsl_test(gsg, "assert(true);") + + +def test_glsl_test_fail(gsg): + "Same as above, but making sure that the failure case works correctly." + + with pytest.raises(Failed): + run_glsl_test(gsg, "assert(false);") + + +def test_glsl_sampler(gsg): + tex1 = core.Texture("") + tex1.setup_1d_texture(1, core.Texture.T_unsigned_byte, core.Texture.F_rgba8) + tex1.set_clear_color((0, 2 / 255.0, 1, 1)) + + tex2 = core.Texture("") + tex2.setup_2d_texture(1, 1, core.Texture.T_float, core.Texture.F_rgba32) + tex2.set_clear_color((1.0, 2.0, -3.14, 0.0)) + + preamble = """ + uniform sampler1D tex1; + uniform sampler2D tex2; + """ + code = """ + assert(texelFetch(tex1, 0, 0) == vec4(0, 2 / 255.0, 1, 1)); + assert(texelFetch(tex2, ivec2(0, 0), 0) == vec4(1.0, 2.0, -3.14, 0.0)); + """ + run_glsl_test(gsg, code, preamble, {'tex1': tex1, 'tex2': tex2}), code + + +def test_glsl_image(gsg): + tex1 = core.Texture("") + tex1.setup_1d_texture(1, core.Texture.T_unsigned_byte, core.Texture.F_rgba8) + tex1.set_clear_color((0, 2 / 255.0, 1, 1)) + + tex2 = core.Texture("") + tex2.setup_2d_texture(1, 1, core.Texture.T_float, core.Texture.F_rgba32) + tex2.set_clear_color((1.0, 2.0, -3.14, 0.0)) + + preamble = """ + layout(rgba8) uniform image1D tex1; + layout(rgba32f) uniform image2D tex2; + """ + code = """ + assert(imageLoad(tex1, 0) == vec4(0, 2 / 255.0, 1, 1)); + assert(imageLoad(tex2, ivec2(0, 0)) == vec4(1.0, 2.0, -3.14, 0.0)); + """ + run_glsl_test(gsg, code, preamble, {'tex1': tex1, 'tex2': tex2}), code + + +def test_glsl_ssbo(gsg): + from struct import pack + num1 = pack('