diff --git a/main.cpp b/main.cpp index ee2559af..9539dc72 100644 --- a/main.cpp +++ b/main.cpp @@ -432,6 +432,7 @@ struct CLIArguments bool force_temporary = false; bool flatten_ubo = false; bool fixup = false; + bool sso = false; vector pls_in; vector pls_out; vector remaps; @@ -458,6 +459,7 @@ static void print_help() "[--cpp] [--cpp-interface-name ] " "[--msl] [--msl-no-pack-ubos] " "[--hlsl] [--shader-model] [--hlsl-enable-compat] " + "[--separate-shader-objects]" "[--pls-in format input-name] [--pls-out format output-name] [--remap source_name target_name " "components] [--extension ext] [--entry name] [--remove-unused-variables] " "[--remap-variable-type ]\n"); @@ -585,6 +587,7 @@ int main(int argc, char *argv[]) cbs.add("--vulkan-semantics", [&args](CLIParser &) { args.vulkan_semantics = true; }); cbs.add("--extension", [&args](CLIParser &parser) { args.extensions.push_back(parser.next_string()); }); cbs.add("--entry", [&args](CLIParser &parser) { args.entry = parser.next_string(); }); + cbs.add("--separate-shader-objects", [&args](CLIParser &) { args.sso = true; }); cbs.add("--remap", [&args](CLIParser &parser) { string src = parser.next_string(); string dst = parser.next_string(); @@ -689,6 +692,7 @@ int main(int argc, char *argv[]) if (args.set_es) opts.es = args.es; opts.force_temporary = args.force_temporary; + opts.separate_shader_objects = args.sso; opts.vulkan_semantics = args.vulkan_semantics; opts.vertex.fixup_clipspace = args.fixup; opts.cfg_analysis = args.cfg_analysis; diff --git a/spirv_glsl.cpp b/spirv_glsl.cpp index 69194c78..a4993a1b 100644 --- a/spirv_glsl.cpp +++ b/spirv_glsl.cpp @@ -300,6 +300,9 @@ void CompilerGLSL::find_static_extensions() if (!pls_inputs.empty() || !pls_outputs.empty()) require_extension("GL_EXT_shader_pixel_local_storage"); + + if (options.separate_shader_objects && !options.es && options.version < 410) + require_extension("GL_ARB_separate_shader_objects"); } string CompilerGLSL::compile() @@ -1495,6 +1498,64 @@ void CompilerGLSL::fixup_image_load_store_access() } } +void CompilerGLSL::emit_declared_builtin_block(StorageClass storage, ExecutionModel model) +{ + bool emitted_block = false; + for (auto &id : ids) + { + if (id.get_type() != TypeVariable) + continue; + + auto &var = id.get(); + auto &type = get(var.basetype); + bool block = has_decoration(type.self, DecorationBlock); + uint64_t builtins = 0; + + if (var.storage == storage && block && is_builtin_variable(var)) + { + for (auto &m : meta[type.self].members) + if (m.builtin) + builtins |= 1ull << m.builtin_type; + } + + if (!builtins) + continue; + + if (emitted_block) + SPIRV_CROSS_THROW("Cannot use more than one builtin I/O block."); + + if (storage == StorageClassOutput) + statement("out gl_PerVertex"); + else + statement("in gl_PerVertex"); + + begin_scope(); + if (builtins & (1ull << BuiltInPosition)) + statement("vec4 gl_Position;"); + if (builtins & (1ull << BuiltInPointSize)) + statement("float gl_PointSize;"); + if (builtins & (1ull << BuiltInClipDistance)) + statement("float gl_ClipDistance[];"); // TODO: Do we need a fixed array size here? + if (builtins & (1ull << BuiltInCullDistance)) + statement("float gl_CullDistance[];"); // TODO: Do we need a fixed array size here? + + bool builtin_array = !type.array.empty(); + bool tessellation = model == ExecutionModelTessellationEvaluation || model == ExecutionModelTessellationControl; + if (builtin_array) + { + if (model == ExecutionModelTessellationControl && storage == StorageClassOutput) + end_scope_decl(join(to_name(var.self), "[", get_entry_point().output_vertices, "]")); + else + end_scope_decl(join(to_name(var.self), tessellation ? "[gl_MaxPatchVertices]" : "[]")); + } + else + end_scope_decl(); + statement(""); + + emitted_block = true; + } +} + void CompilerGLSL::emit_resources() { auto &execution = get_entry_point(); @@ -1510,6 +1571,27 @@ void CompilerGLSL::emit_resources() if (!pls_inputs.empty() || !pls_outputs.empty()) emit_pls(); + // Emit custom gl_PerVertex for SSO compatibility. + if (options.separate_shader_objects && !options.es) + { + switch (execution.model) + { + case ExecutionModelGeometry: + case ExecutionModelTessellationControl: + case ExecutionModelTessellationEvaluation: + emit_declared_builtin_block(StorageClassInput, execution.model); + emit_declared_builtin_block(StorageClassOutput, execution.model); + break; + + case ExecutionModelVertex: + emit_declared_builtin_block(StorageClassOutput, execution.model); + break; + + default: + break; + } + } + bool emitted = false; // If emitted Vulkan GLSL, diff --git a/spirv_glsl.hpp b/spirv_glsl.hpp index f27b14c6..6d06518b 100644 --- a/spirv_glsl.hpp +++ b/spirv_glsl.hpp @@ -67,6 +67,12 @@ public: // Mostly useful for debugging SPIR-V files. bool vulkan_semantics = false; + // If true, gl_PerVertex is explicitly redeclared in vertex, geometry and tessellation shaders. + // The members of gl_PerVertex is determined by which built-ins are declared by the shader. + // This option is ignored in ES versions, as redeclaration in ES is not required, and it depends on a different extension + // (EXT_shader_io_blocks) which makes things a bit more fuzzy. + bool separate_shader_objects = false; + enum Precision { DontCare, @@ -284,6 +290,7 @@ protected: void emit_buffer_block_native(const SPIRVariable &var); void emit_buffer_block_legacy(const SPIRVariable &var); void emit_buffer_block_flattened(const SPIRVariable &type); + void emit_declared_builtin_block(spv::StorageClass storage, spv::ExecutionModel model); void emit_push_constant_block_vulkan(const SPIRVariable &var); void emit_push_constant_block_glsl(const SPIRVariable &var); void emit_interface_block(const SPIRVariable &type); diff --git a/test_shaders.py b/test_shaders.py index 4788691d..2d2b8979 100755 --- a/test_shaders.py +++ b/test_shaders.py @@ -120,7 +120,7 @@ def validate_shader(shader, vulkan): else: subprocess.check_call(['glslangValidator', shader]) -def cross_compile(shader, vulkan, spirv, invalid_spirv, eliminate, is_legacy, flatten_ubo): +def cross_compile(shader, vulkan, spirv, invalid_spirv, eliminate, is_legacy, flatten_ubo, sso): spirv_f, spirv_path = tempfile.mkstemp() glsl_f, glsl_path = tempfile.mkstemp(suffix = os.path.basename(shader)) os.close(spirv_f) @@ -145,6 +145,8 @@ def cross_compile(shader, vulkan, spirv, invalid_spirv, eliminate, is_legacy, fl extra_args += ['--version', '100', '--es'] if flatten_ubo: extra_args += ['--flatten-ubo'] + if sso: + extra_args += ['--separate-shader-objects'] spirv_cross_path = './spirv-cross' subprocess.check_call([spirv_cross_path, '--entry', 'main', '--output', glsl_path, spirv_path] + extra_args) @@ -238,6 +240,9 @@ def shader_is_legacy(shader): def shader_is_flatten_ubo(shader): return '.flatten.' in shader +def shader_is_sso(shader): + return '.sso.' in shader + def test_shader(stats, shader, update, keep): joined_path = os.path.join(shader[0], shader[1]) vulkan = shader_is_vulkan(shader[1]) @@ -247,9 +252,10 @@ def test_shader(stats, shader, update, keep): invalid_spirv = shader_is_invalid_spirv(shader[1]) is_legacy = shader_is_legacy(shader[1]) flatten_ubo = shader_is_flatten_ubo(shader[1]) + sso = shader_is_sso(shader[1]) print('Testing shader:', joined_path) - spirv, glsl, vulkan_glsl = cross_compile(joined_path, vulkan, is_spirv, invalid_spirv, eliminate, is_legacy, flatten_ubo) + spirv, glsl, vulkan_glsl = cross_compile(joined_path, vulkan, is_spirv, invalid_spirv, eliminate, is_legacy, flatten_ubo, sso) # Only test GLSL stats if we have a shader following GL semantics. if stats and (not vulkan) and (not is_spirv) and (not desktop):