conftest.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. from panda3d import core
  2. import pytest
  3. from _pytest.outcomes import Failed
  4. import sys
  5. # Instantiate all of the available graphics pipes.
  6. ALL_PIPES = []
  7. sel = core.GraphicsPipeSelection.get_global_ptr()
  8. pipe = sel.make_module_pipe('p3headlessgl')
  9. if pipe and pipe.is_valid():
  10. ALL_PIPES.append(pipe)
  11. else:
  12. pipe = sel.make_module_pipe('pandagl')
  13. if pipe and pipe.is_valid():
  14. ALL_PIPES.append(pipe)
  15. #pipe = sel.make_module_pipe('pandagles2')
  16. #if pipe and pipe.is_valid():
  17. # ALL_PIPES.append(pipe)
  18. if sys.platform == 'win32':
  19. pipe = sel.make_module_pipe('pandadx9')
  20. if pipe and pipe.is_valid():
  21. ALL_PIPES.append(pipe)
  22. pipe = sel.make_module_pipe('p3vulkandisplay')
  23. if pipe and pipe.is_valid():
  24. ALL_PIPES.append(pipe)
  25. @pytest.fixture(scope='session')
  26. def graphics_pipes():
  27. yield ALL_PIPES
  28. @pytest.fixture(scope='session', params=ALL_PIPES)
  29. def graphics_pipe(request):
  30. pipe = request.param
  31. if pipe is None or not pipe.is_valid():
  32. pytest.skip("GraphicsPipe is invalid")
  33. yield pipe
  34. @pytest.fixture(scope='session')
  35. def graphics_engine():
  36. from panda3d.core import GraphicsEngine
  37. engine = GraphicsEngine.get_global_ptr()
  38. yield engine
  39. # This causes GraphicsEngine to also terminate the render threads.
  40. engine.remove_all_windows()
  41. @pytest.fixture
  42. def window(graphics_pipe, graphics_engine):
  43. from panda3d.core import GraphicsPipe, FrameBufferProperties, WindowProperties
  44. fbprops = FrameBufferProperties.get_default()
  45. winprops = WindowProperties.get_default()
  46. win = graphics_engine.make_output(
  47. graphics_pipe,
  48. 'window',
  49. 0,
  50. fbprops,
  51. winprops,
  52. GraphicsPipe.BF_require_window
  53. )
  54. graphics_engine.open_windows()
  55. if win is None:
  56. pytest.skip("GraphicsPipe cannot make windows")
  57. yield win
  58. if win is not None:
  59. graphics_engine.remove_window(win)
  60. # This is the template for the compute shader that is used by run_glsl_test.
  61. # It defines an assert() macro that writes failures to a buffer, indexed by
  62. # line number.
  63. # The reset() function serves to prevent the _triggered variable from being
  64. # optimized out in the case that the assertions are being optimized out.
  65. GLSL_COMPUTE_TEMPLATE = """#version {version}
  66. {extensions}
  67. layout(local_size_x = 1, local_size_y = 1) in;
  68. {preamble}
  69. layout(r32ui) uniform writeonly uimageBuffer _triggered;
  70. void _reset() {{
  71. imageStore(_triggered, 0, uvec4(1));
  72. memoryBarrier();
  73. }}
  74. void _assert(bool cond, int line) {{
  75. if (!cond) {{
  76. imageStore(_triggered, line, uvec4(1));
  77. }}
  78. }}
  79. #define assert(cond) _assert(cond, __LINE__ - line_offset)
  80. void main() {{
  81. _reset();
  82. const int line_offset = __LINE__;
  83. {body}
  84. }}
  85. """
  86. # This is a version that uses a vertex and fragment shader instead. This is
  87. # slower to set up, but it works even when compute shaders are not supported.
  88. # The shader is rendered on a fullscreen triangle to a texture, where each
  89. # pixel represents one line of the code. The assert writes the result to the
  90. # output color if the current fragment matches the line number of that assert.
  91. # The first pixel is used as a control, to check that the shader has run.
  92. GLSL_VERTEX_TEMPLATE = """#version {version}
  93. in vec4 p3d_Vertex;
  94. void main() {{
  95. gl_Position = p3d_Vertex;
  96. }}
  97. """
  98. GLSL_FRAGMENT_TEMPLATE = """#version {version}
  99. {extensions}
  100. {preamble}
  101. layout(location = 0) out vec4 p3d_FragColor;
  102. void _reset() {{
  103. p3d_FragColor = vec4(0, 0, 0, 0);
  104. if (int(gl_FragCoord.x) == 0) {{
  105. p3d_FragColor = vec4(1, 1, 1, 1);
  106. }}
  107. }}
  108. void _assert(bool cond, int line) {{
  109. if (int(gl_FragCoord.x) == line) {{
  110. p3d_FragColor = vec4(!cond, !cond, !cond, !cond);
  111. }}
  112. }}
  113. #define assert(cond) _assert(cond, __LINE__ - line_offset)
  114. void main() {{
  115. _reset();
  116. const int line_offset = __LINE__;
  117. {body}
  118. }}
  119. """
  120. # This is the template for the shader that is used by run_cg_test.
  121. # We render this to an nx1 texture, where n is the number of lines in the body.
  122. # An assert
  123. CG_VERTEX_TEMPLATE = """//Cg
  124. void vshader(float4 vtx_position : POSITION, out float4 l_position : POSITION) {{
  125. l_position = vtx_position;
  126. }}
  127. """
  128. CG_FRAGMENT_TEMPLATE = """//Cg
  129. {preamble}
  130. float4 _assert(bool cond) {{
  131. return float4(cond.x, 1, 1, 1);
  132. }}
  133. float4 _assert(bool2 cond) {{
  134. return float4(cond.x, cond.y, 1, 1);
  135. }}
  136. float4 _assert(bool3 cond) {{
  137. return float4(cond.x, cond.y, cond.z, 1);
  138. }}
  139. float4 _assert(bool4 cond) {{
  140. return float4(cond.x, cond.y, cond.z, cond.w);
  141. }}
  142. #define assert(cond) {{ if ((int)l_vpos.x == __LINE__ - line_offset) o_color = _assert(cond); }}
  143. void fshader(in float2 l_vpos : VPOS, out float4 o_color : COLOR) {{
  144. o_color = float4(1, 1, 1, 1);
  145. if ((int)l_vpos.x == 0) {{
  146. o_color = float4(0, 0, 0, 0);
  147. }}
  148. const int line_offset = __LINE__;
  149. {body}
  150. }}
  151. """
  152. class ShaderEnvironment:
  153. def __init__(self, name, gsg, allow_compute=True):
  154. self.name = name
  155. self.gsg = gsg
  156. self.engine = gsg.get_engine()
  157. self.allow_compute = allow_compute
  158. def __repr__(self):
  159. return f'<{self.name} vendor="{self.gsg.driver_vendor}" renderer="{self.gsg.driver_renderer}" version="{self.gsg.driver_version}">'
  160. def extract_texture_data(self, tex):
  161. __tracebackhide__ = True
  162. result = self.engine.extract_texture_data(tex, self.gsg)
  163. assert result
  164. def run_glsl(self, body, preamble="", inputs={}, version=420, exts=set(),
  165. state=core.RenderState.make_empty()):
  166. """ Runs a GLSL test on the given GSG. The given body is executed in the
  167. main function and should call assert(). The preamble should contain all
  168. of the shader inputs. """
  169. gsg = self.gsg
  170. if not gsg.supports_basic_shaders:
  171. pytest.skip("shaders not supported")
  172. use_compute = self.allow_compute and gsg.supports_compute_shaders and \
  173. (gsg.supported_shader_capabilities & core.Shader.C_image_load_store) != 0
  174. missing_exts = sorted(ext for ext in exts if not gsg.has_extension(ext))
  175. if missing_exts:
  176. pytest.skip("missing extensions: " + ' '.join(missing_exts))
  177. version = version or 420
  178. exts = exts | {'GL_ARB_compute_shader', 'GL_ARB_shader_image_load_store'}
  179. extensions = ''
  180. for ext in exts:
  181. extensions += '#extension {ext} : require\n'.format(ext=ext)
  182. __tracebackhide__ = True
  183. preamble = preamble.strip()
  184. body = body.rstrip().lstrip('\n')
  185. if use_compute:
  186. code = GLSL_COMPUTE_TEMPLATE.format(version=version, extensions=extensions, preamble=preamble, body=body)
  187. shader = core.Shader.make_compute(core.Shader.SL_GLSL, code)
  188. else:
  189. vertex_code = GLSL_VERTEX_TEMPLATE.format(version=version, extensions=extensions, preamble=preamble, body=body)
  190. code = GLSL_FRAGMENT_TEMPLATE.format(version=version, extensions=extensions, preamble=preamble, body=body)
  191. shader = core.Shader.make(core.Shader.SL_GLSL, vertex_code, code)
  192. if not shader:
  193. pytest.fail("error compiling shader:\n" + code)
  194. unsupported_caps = shader.get_used_capabilities() & ~gsg.supported_shader_capabilities
  195. if unsupported_caps != 0:
  196. stream = core.StringStream()
  197. core.ShaderEnums.output_capabilities(stream, unsupported_caps)
  198. pytest.skip("unsupported capabilities: " + stream.data.decode('ascii'))
  199. num_lines = body.count('\n') + 1
  200. # Create a buffer to hold the results of the assertion. We use one texel
  201. # per line of shader code, so we can show which lines triggered.
  202. engine = gsg.get_engine()
  203. result = core.Texture("")
  204. if use_compute:
  205. result.set_clear_color((0, 0, 0, 0))
  206. result.setup_buffer_texture(num_lines + 1,
  207. core.Texture.T_unsigned_int,
  208. core.Texture.F_r32i,
  209. core.GeomEnums.UH_static)
  210. else:
  211. fbprops = core.FrameBufferProperties()
  212. fbprops.force_hardware = True
  213. fbprops.set_rgba_bits(8, 8, 8, 8)
  214. fbprops.srgb_color = False
  215. buffer = engine.make_output(
  216. gsg.pipe,
  217. 'buffer',
  218. 0,
  219. fbprops,
  220. core.WindowProperties.size(core.Texture.up_to_power_2(num_lines + 1), 1),
  221. core.GraphicsPipe.BF_refuse_window,
  222. gsg
  223. )
  224. buffer.add_render_texture(result, core.GraphicsOutput.RTM_copy_ram, core.GraphicsOutput.RTP_color)
  225. buffer.set_clear_color_active(True)
  226. buffer.set_clear_color((0, 0, 0, 0))
  227. engine.open_windows()
  228. # Build up the shader inputs
  229. attrib = core.ShaderAttrib.make(shader)
  230. for name, value in inputs.items():
  231. attrib = attrib.set_shader_input(name, value)
  232. if use_compute:
  233. attrib = attrib.set_shader_input('_triggered', result)
  234. state = state.set_attrib(attrib)
  235. # Run the shader.
  236. if use_compute:
  237. try:
  238. gsg.make_current()
  239. engine.dispatch_compute((1, 1, 1), state, gsg)
  240. except AssertionError as exc:
  241. assert False, "Error executing compute shader:\n" + code
  242. else:
  243. scene = core.NodePath("root")
  244. scene.set_attrib(core.DepthTestAttrib.make(core.RenderAttrib.M_always))
  245. format = core.GeomVertexFormat.get_v3()
  246. vdata = core.GeomVertexData("tri", format, core.Geom.UH_static)
  247. vdata.unclean_set_num_rows(3)
  248. vertex = core.GeomVertexWriter(vdata, "vertex")
  249. vertex.set_data3(-1, -1, 0)
  250. vertex.set_data3(3, -1, 0)
  251. vertex.set_data3(-1, 3, 0)
  252. tris = core.GeomTriangles(core.Geom.UH_static)
  253. tris.add_next_vertices(3)
  254. geom = core.Geom(vdata)
  255. geom.add_primitive(tris)
  256. gnode = core.GeomNode("tri")
  257. gnode.add_geom(geom, state)
  258. scene.attach_new_node(gnode)
  259. scene.set_two_sided(True)
  260. camera = scene.attach_new_node(core.Camera("camera"))
  261. camera.node().get_lens(0).set_near_far(-10, 10)
  262. camera.node().set_cull_bounds(core.OmniBoundingVolume())
  263. region = buffer.make_display_region()
  264. region.active = True
  265. region.camera = camera
  266. try:
  267. engine.render_frame()
  268. except AssertionError as exc:
  269. assert False, "Error executing shader:\n" + code
  270. finally:
  271. engine.remove_window(buffer)
  272. # Download the texture to check whether the assertion triggered.
  273. if use_compute:
  274. success = engine.extract_texture_data(result, gsg)
  275. assert success
  276. triggered = result.get_ram_image()
  277. triggered = tuple(memoryview(triggered).cast('I'))
  278. if not triggered[0]:
  279. pytest.fail("control check failed")
  280. if any(triggered[1:]):
  281. count = len(triggered) - triggered.count(0) - 1
  282. lines = body.split('\n')
  283. formatted = ''
  284. for i, line in enumerate(lines):
  285. if triggered[i + 1]:
  286. formatted += '=> ' + line + '\n'
  287. else:
  288. formatted += ' ' + line + '\n'
  289. pytest.fail("{0} GLSL assertions triggered:\n{1}".format(count, formatted))
  290. def run_cg(self, body, preamble="", inputs={}, state=core.RenderState.make_empty()):
  291. """ Runs a Cg test on the given GSG. The given body is executed in the
  292. main function and should call assert(). The preamble should contain all
  293. of the shader inputs. """
  294. if self.name.endswith("-legacy"):
  295. pytest.skip("no Cg support in legacy pipeline")
  296. gsg = self.gsg
  297. if not gsg.supports_basic_shaders:
  298. pytest.skip("basic shaders not supported")
  299. __tracebackhide__ = True
  300. preamble = preamble.strip()
  301. body = body.rstrip().lstrip('\n')
  302. num_lines = body.count('\n') + 1
  303. vertex_code = CG_VERTEX_TEMPLATE.format(preamble=preamble, body=body)
  304. code = CG_FRAGMENT_TEMPLATE.format(preamble=preamble, body=body)
  305. shader = core.Shader.make(core.Shader.SL_Cg, vertex_code, code)
  306. if not shader:
  307. pytest.fail("error compiling shader:\n" + code)
  308. result = core.Texture("")
  309. fbprops = core.FrameBufferProperties()
  310. fbprops.force_hardware = True
  311. fbprops.set_rgba_bits(8, 8, 8, 8)
  312. fbprops.srgb_color = False
  313. engine = gsg.get_engine()
  314. buffer = engine.make_output(
  315. gsg.pipe,
  316. 'buffer',
  317. 0,
  318. fbprops,
  319. core.WindowProperties.size(core.Texture.up_to_power_2(num_lines + 1), 1),
  320. core.GraphicsPipe.BF_refuse_window,
  321. gsg
  322. )
  323. buffer.add_render_texture(result, core.GraphicsOutput.RTM_copy_ram, core.GraphicsOutput.RTP_color)
  324. buffer.set_clear_color_active(True)
  325. buffer.set_clear_color((0, 0, 0, 0))
  326. engine.open_windows()
  327. # Build up the shader inputs
  328. attrib = core.ShaderAttrib.make(shader)
  329. for name, value in inputs.items():
  330. attrib = attrib.set_shader_input(name, value)
  331. state = state.set_attrib(attrib)
  332. scene = core.NodePath("root")
  333. scene.set_attrib(core.DepthTestAttrib.make(core.RenderAttrib.M_always))
  334. format = core.GeomVertexFormat.get_v3()
  335. vdata = core.GeomVertexData("tri", format, core.Geom.UH_static)
  336. vdata.unclean_set_num_rows(3)
  337. vertex = core.GeomVertexWriter(vdata, "vertex")
  338. vertex.set_data3(-1, -1, 0)
  339. vertex.set_data3(3, -1, 0)
  340. vertex.set_data3(-1, 3, 0)
  341. tris = core.GeomTriangles(core.Geom.UH_static)
  342. tris.add_next_vertices(3)
  343. geom = core.Geom(vdata)
  344. geom.add_primitive(tris)
  345. gnode = core.GeomNode("tri")
  346. gnode.add_geom(geom, state)
  347. scene.attach_new_node(gnode)
  348. scene.set_two_sided(True)
  349. camera = scene.attach_new_node(core.Camera("camera"))
  350. camera.node().get_lens(0).set_near_far(-10, 10)
  351. camera.node().set_cull_bounds(core.OmniBoundingVolume())
  352. region = buffer.make_display_region()
  353. region.active = True
  354. region.camera = camera
  355. try:
  356. engine.render_frame()
  357. except AssertionError as exc:
  358. assert False, "Error executing shader:\n" + code
  359. finally:
  360. engine.remove_window(buffer)
  361. # Download the texture to check whether the assertion triggered.
  362. triggered = tuple(result.get_ram_image())
  363. if triggered[0]:
  364. pytest.fail("control check failed")
  365. if not all(triggered[4:]):
  366. count = 0
  367. lines = body.split('\n')
  368. formatted = ''
  369. for i, line in enumerate(lines):
  370. offset = (i + 1) * 4
  371. x = triggered[offset + 2] == 0
  372. y = triggered[offset + 1] == 0
  373. z = triggered[offset] == 0
  374. w = triggered[offset + 3] == 0
  375. if x or y or z or w:
  376. count += 1
  377. else:
  378. continue
  379. formatted += '=> ' + line
  380. components = ''
  381. if x:
  382. components += 'x'
  383. if y:
  384. components += 'y'
  385. if z:
  386. components += 'z'
  387. if w:
  388. components += 'w'
  389. formatted += f' <= {components} components don\'t match'
  390. formatted += '\n'
  391. pytest.fail("{0} Cg assertions triggered:\n{1}".format(count, formatted))
  392. # Which environments should we test shaders in?
  393. ENVS = set()
  394. for pipe in ALL_PIPES:
  395. if pipe.interface_name == 'OpenGL':
  396. ENVS |= frozenset(("gl-legacy", "gl-spirv", "gl-cross-120", "gl-cross-130", "gl-cross-140", "gl-cross-150", "gl-cross-330", "gl-cross-400", "gl-cross-410", "gl-cross-420", "gl-cross-430"))
  397. elif pipe.interface_name == 'OpenGL ES':
  398. ENVS |= frozenset(("gles-cross-100", "gles-cross-300", "gles-cross-310", "gles-cross-320"))
  399. elif pipe.interface_name == 'DirectX9':
  400. ENVS |= frozenset(("dx9-cross", ))
  401. elif pipe.interface_name == 'Vulkan':
  402. ENVS |= frozenset(("vk-spirv", ))
  403. @pytest.fixture(scope="session", params=sorted(ENVS))
  404. def env(request):
  405. config = {}
  406. if request.param.startswith("gl-"):
  407. for pipe in ALL_PIPES:
  408. if pipe.interface_name == 'OpenGL':
  409. break
  410. else:
  411. pytest.skip("no OpenGL pipe found")
  412. elif request.param.startswith("gles-"):
  413. for pipe in ALL_PIPES:
  414. if pipe.interface_name == 'OpenGL ES':
  415. break
  416. else:
  417. pytest.skip("no OpenGL ES pipe found")
  418. elif request.param.startswith("dx9-"):
  419. for pipe in ALL_PIPES:
  420. if pipe.interface_name == 'DirectX9':
  421. break
  422. else:
  423. pytest.skip("no DirectX 9 pipe found")
  424. elif request.param.startswith("vk-"):
  425. for pipe in ALL_PIPES:
  426. if pipe.interface_name == 'Vulkan':
  427. break
  428. else:
  429. pytest.skip("no Vulkan pipe found")
  430. words = request.param.split("-")
  431. if words[0] == "gl" or words[0] == "gles":
  432. # Set the coordinate system to z-up-right, so that we get the same
  433. # results with compute shaders (which always have an identity CS
  434. # transform) and regular shaders.
  435. config["gl-coordinate-system"] = "zup-right"
  436. if words[0] == "gles":
  437. # Necessary for image load/store support
  438. config["gl-immutable-texture-storage"] = "true"
  439. if words[1] == "legacy":
  440. config["glsl-force-legacy-pipeline"] = "true"
  441. allow_compute = True
  442. elif words[1] == "spirv":
  443. config["glsl-force-legacy-pipeline"] = "false"
  444. config["gl-support-spirv"] = "true"
  445. allow_compute = True
  446. elif words[1] == "cross":
  447. config["glsl-force-legacy-pipeline"] = "false"
  448. config["gl-support-spirv"] = "false"
  449. version = int(words[2])
  450. allow_compute = (version >= 330)
  451. config["gl-force-glsl-version"] = str(version)
  452. else:
  453. allow_compute = False
  454. prc = '\n'.join(f"{key} {value}" for key, value in config.items())
  455. page = core.load_prc_file_data("", prc)
  456. engine = core.GraphicsEngine()
  457. fbprops = core.FrameBufferProperties()
  458. fbprops.set_rgba_bits(8, 8, 8, 8)
  459. fbprops.force_hardware = True
  460. props = core.WindowProperties.size(32, 32)
  461. buffer = engine.make_output(
  462. pipe,
  463. 'buffer',
  464. 0,
  465. fbprops,
  466. props,
  467. core.GraphicsPipe.BF_refuse_window
  468. )
  469. if buffer is None:
  470. # Try making a window instead, putting it in the background so it
  471. # disrupts the desktop as little as possible
  472. props.minimized = True
  473. props.foreground = False
  474. props.z_order = core.WindowProperties.Z_bottom
  475. buffer = engine.make_output(
  476. pipe,
  477. 'buffer',
  478. 0,
  479. fbprops,
  480. props,
  481. core.GraphicsPipe.BF_require_window
  482. )
  483. if buffer is None:
  484. pytest.skip("GraphicsPipe cannot make offscreen buffers or windows")
  485. engine.open_windows()
  486. gsg = buffer.gsg
  487. # Check if the environment is actually supported.
  488. if words[0] == "gl" or words[0] == "gles":
  489. if words[1] == "legacy":
  490. if not gsg.supports_glsl:
  491. pytest.skip("legacy GLSL shaders not supported")
  492. elif words[1] == "spirv":
  493. if (gsg.driver_version_major, gsg.driver_version_minor) < (4, 6) and \
  494. not gsg.has_extension("GL_ARB_gl_spirv"):
  495. pytest.skip("SPIR-V shaders not supported")
  496. elif words[1] == "cross":
  497. version = int(words[2])
  498. if version > gsg.driver_shader_version_major * 100 + gsg.driver_shader_version_minor:
  499. pytest.skip(f"GLSL {version} shaders not supported")
  500. env = ShaderEnvironment(request.param, gsg, allow_compute)
  501. try:
  502. yield env
  503. finally:
  504. core.unload_prc_file(page)
  505. if buffer is not None:
  506. engine.remove_window(buffer)