conftest.py 20 KB

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