diff --git a/src/Magnum/ShaderTools/CMakeLists.txt b/src/Magnum/ShaderTools/CMakeLists.txt index 041995e5d..11c51889b 100644 --- a/src/Magnum/ShaderTools/CMakeLists.txt +++ b/src/Magnum/ShaderTools/CMakeLists.txt @@ -40,6 +40,9 @@ set(MagnumShaderTools_HEADERS visibility.h) +set(MagnumShaderTools_PRIVATE_HEADERS + Implementation/spirv.h) + if(NOT CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/configure.h) @@ -48,7 +51,8 @@ endif() # # Objects shared between main and test library # add_library(MagnumShaderToolsObjects OBJECT # ${MagnumShaderTools_SRCS} -# ${MagnumShaderTools_HEADERS}) +# # ${MagnumShaderTools_HEADERS} +# # ${MagnumShaderTools_PRIVATE_HEADERS}) # target_include_directories(MagnumShaderToolsObjects PUBLIC $) # if(NOT BUILD_STATIC) # target_compile_definitions(MagnumShaderToolsObjects PRIVATE "MagnumShaderToolsObjects_EXPORTS") @@ -61,7 +65,9 @@ endif() # Main ShaderTools library add_library(MagnumShaderTools ${SHARED_OR_STATIC} # $ - ${MagnumShaderTools_GracefulAssert_SRCS}) + ${MagnumShaderTools_GracefulAssert_SRCS} + ${MagnumShaderTools_HEADERS} + ${MagnumShaderTools_PRIVATE_HEADERS}) set_target_properties(MagnumShaderTools PROPERTIES DEBUG_POSTFIX "-d" FOLDER "Magnum/ShaderTools") diff --git a/src/Magnum/ShaderTools/Implementation/spirv.h b/src/Magnum/ShaderTools/Implementation/spirv.h new file mode 100644 index 000000000..610ef019e --- /dev/null +++ b/src/Magnum/ShaderTools/Implementation/spirv.h @@ -0,0 +1,183 @@ +#ifndef Magnum_ShaderTools_Implementation_spirv_h +#define Magnum_ShaderTools_Implementation_spirv_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + + 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 +#include +#include + +#include "Magnum/Magnum.h" +#include "MagnumExternal/Vulkan/spirv.h" + +namespace Magnum { namespace ShaderTools { namespace Implementation { namespace { + +/* This is used by both magnum-shaderconverter and the Vk library for + SwiftShader workarounds but we don't want the Vk library to depend on + ShaderTools, so the minimal needed subset is made header-only. + + Eventually this should be turned into a public API, but so far it's just a + bag of random functions with very specific usage patterns and isn't clear + yet how to expose a usable interface. Moreover, the SwiftShader patching + needs to mutate the original, which means the outputs are pointers to the + original data. */ + +/* If the code looks like a valid SPIR-V, returns everything after the + header. If not, nullptr. */ +Containers::ArrayView spirvData(const void* code, UnsignedInt size) { + const UnsignedInt* const spirv = static_cast(code); + /* Not >= 5*4 because just the header alone is useless also */ + return size % 4 == 0 && size > 5*4 && spirv[0] == SpvMagicNumber ? + Containers::ArrayView{spirv, size/4}.suffix(5) : nullptr; +} + +/* When an instruction is found, the `data` is advanced after it in order to + allow calling this function in a loop. When not found, `data` is left + untouched. */ +Containers::ArrayView spirvFindInstruction(Containers::ArrayView& data, const SpvOp op) { + /* Copy the view and iterate that. If we find the instruction, update the + passed `data` reference, if not, keep it as it was -- that way, if the + find fails, `data` won't become empty and can be used further */ + for(Containers::ArrayView dataIteration = data; !dataIteration.empty(); ) { + const UnsignedInt instructionSize = dataIteration[0] >> 16; + const UnsignedInt instructionOp = dataIteration[0] & 0xffff; + + /* Corrupted SPIR-V */ + /** @todo print a message here? */ + if(dataIteration.size() < instructionSize) { + data = dataIteration; + return nullptr; + } + + /* This is the instruction we're looking for, return it and update the + view to point after it. */ + if(instructionOp == op) { + data = dataIteration.suffix(instructionSize); + return dataIteration.prefix(instructionSize); + } + + /* Otherwise advance the view for next round */ + dataIteration = dataIteration.suffix(instructionSize); + } + + /* Nothing found. Leave the input data as-is. */ + return nullptr; +} + +struct SpirvEntrypoint { + Containers::Reference executionModel; + Containers::StringView name; + Containers::ArrayView interfaces; +}; + +/* When an entrypoint is found, the `data` is advanced after the instruction in + order to allow calling this function in a loop. When not found, `data` is + left untouched. Most of other SPIR-V code is meant to appear after the + entrypoints, so it's fine to feed the resulting `data` to + spirvEntrypointInterface() and others. */ +Containers::Optional spirvNextEntrypoint(Containers::ArrayView& data) { + while(const Containers::ArrayView entryPoint = spirvFindInstruction(data, SpvOpEntryPoint)) { + /* Expecting at least op, execution model, ID, name. If less, it's an + invalid SPIR-V. */ + /** @todo print a message here? */ + if(entryPoint.size() < 4) return {}; + + /* Find where the name ends and interface IDs start. According to the + spec, a string literal is null-terminated and all bytes after are + zeros as well, so it should be enough to check that the last byte is + zero. */ + Containers::ArrayView interfaces; + for(std::size_t i = 3; i != entryPoint.size(); ++i) { + if(entryPoint[i] >> 24 == 0) { + interfaces = entryPoint.suffix(i + 1); + break; + } + } + + return SpirvEntrypoint{ + *reinterpret_cast(entryPoint + 1), + reinterpret_cast(entryPoint + 3), + interfaces + }; + } + + return {}; +} + +struct SpirvEntrypointInterface { + /* If null, the interface might be for example builtin */ + const UnsignedInt* location; + /* If null, the SPIR-V is probably invalid */ + const SpvStorageClass* storageClass; +}; + +/* Unlike above, `data` isn't modified by this function -- because the + decoration and variable instructions are likely intermixed for different + entrypoint, it makes sense to restart the search from the beginning for each + entrypoint. + + The `out` array is expected to have the same size as entrypoint.interfaces + and be zero-initialized (so the not found data stay null). */ +void spirvEntrypointInterface(Containers::ArrayView data, const SpirvEntrypoint& entrypoint, Containers::ArrayView out) { + CORRADE_INTERNAL_ASSERT(out.size() == entrypoint.interfaces.size()); + + /* Find location decorations */ + while(const Containers::ArrayView decoration = spirvFindInstruction(data, SpvOpDecorate)) { + /* Expecting at least op, ID, SpvDecorationLocation, location. The + instruction can be three words, so if we get less than 4 it's not an + error. */ + if(decoration.size() < 4 || decoration[2] != SpvDecorationLocation) + continue; + + for(std::size_t i = 0; i != entrypoint.interfaces.size(); ++i) { + if(decoration[1] == entrypoint.interfaces[i]) { + out[i].location = decoration + 3; + break; + } + } + } + + /* Find storage classes. According to the spec, OpVariable is meant to + appear after OpDecorate, so we don't need to restart from the + beginning. */ + while(const Containers::ArrayView variable = spirvFindInstruction(data, SpvOpVariable)) { + /* Expecting at least op, result, ID, SpvStorageClass. If less, it's an + invalid SPIR-V. */ + /** @todo print a message here? */ + if(variable.size() < 4) return; + + for(std::size_t i = 0; i != entrypoint.interfaces.size(); ++i) { + if(variable[2] == entrypoint.interfaces[i]) { + out[i].storageClass = reinterpret_cast(variable + 3); + break; + } + } + } +} + +}}}} + +#endif diff --git a/src/Magnum/ShaderTools/Test/CMakeLists.txt b/src/Magnum/ShaderTools/Test/CMakeLists.txt index a17fbfce7..bb15276e9 100644 --- a/src/Magnum/ShaderTools/Test/CMakeLists.txt +++ b/src/Magnum/ShaderTools/Test/CMakeLists.txt @@ -38,9 +38,14 @@ corrade_add_test(ShaderToolsAbstractConverterTest AbstractConverterTest.cpp LIBRARIES MagnumShaderToolsTestLib FILES file.dat another.dat) target_include_directories(ShaderToolsAbstractConverterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +corrade_add_test(ShaderToolsSpirvTest SpirvTest.cpp + LIBRARIES MagnumShaderTools + FILES SpirvTestFiles/entrypoint-interface.spv) +target_include_directories(ShaderToolsSpirvTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) corrade_add_test(ShaderToolsStageTest StageTest.cpp LIBRARIES MagnumShaderTools) set_target_properties( ShaderToolsAbstractConverterTest + ShaderToolsSpirvTest ShaderToolsStageTest PROPERTIES FOLDER "Magnum/ShaderTools/Test") diff --git a/src/Magnum/ShaderTools/Test/SpirvTest.cpp b/src/Magnum/ShaderTools/Test/SpirvTest.cpp new file mode 100644 index 000000000..d2eae7a2d --- /dev/null +++ b/src/Magnum/ShaderTools/Test/SpirvTest.cpp @@ -0,0 +1,264 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + + 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 +#include +#include + +#include "Magnum/ShaderTools/Implementation/spirv.h" +#include "MagnumExternal/Vulkan/spirv.h" + +#include "configure.h" + +namespace Magnum { namespace ShaderTools { namespace Test { namespace { + +struct SpirvTest: TestSuite::Tester { + explicit SpirvTest(); + + void data(); + void dataInvalid(); + + void findInstruction(); + void findInstructionNotEnoughData(); + + void nextEntrypoint(); + void nextEntrypointInvalidInstruction(); + + void entrypointInterface(); + void entrypointInterfaceNothing(); +}; + +const UnsignedInt Data[] { + SpvMagicNumber, SpvVersion, 0, 66, 0, + 0 /* first instruction */ +}; + +const UnsignedInt JustHeader[]{ + SpvMagicNumber, SpvVersion, 0, 66, 0 +}; + +const UnsignedInt InvalidMagic[]{ + SpvMagicNumber + 1, SpvVersion, 0, 66, 0, + 0 /* first instruction */ +}; + +const struct { + const char* name; + Containers::ArrayView data; +} InvalidData[] { + {"empty", {}}, + {"just the header", JustHeader}, + {"invalid magic", InvalidMagic}, + {"size not divisible by four", Containers::arrayCast(Data).except(1)} +}; + +SpirvTest::SpirvTest() { + addTests({&SpirvTest::data}); + + addInstancedTests({&SpirvTest::dataInvalid}, + Containers::arraySize(InvalidData)); + + addTests({&SpirvTest::findInstruction, + &SpirvTest::findInstructionNotEnoughData, + + &SpirvTest::nextEntrypoint, + &SpirvTest::nextEntrypointInvalidInstruction, + + &SpirvTest::entrypointInterface, + &SpirvTest::entrypointInterfaceNothing}); +} + +void SpirvTest::data() { + CORRADE_COMPARE(Implementation::spirvData(Data, sizeof(Data)), Containers::arrayView(Data + 5, 1)); +} + +void SpirvTest::dataInvalid() { + auto&& data = InvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + CORRADE_VERIFY(!Implementation::spirvData(data.data, data.data.size())); +} + +UnsignedInt op(UnsignedInt length, SpvOp op) { + return length << 16 | op; +} + +void SpirvTest::findInstruction() { + const UnsignedInt data[] { + op(3, SpvOpMemoryModel), SpvAddressingModelLogical, SpvMemoryModelGLSL450, + op(4, SpvOpDecorate), 12, SpvDecorationLocation, 0, + op(1, SpvOpNop), + op(1, SpvOpNop), + op(4, SpvOpDecorate), 13, SpvDecorationLocation, 1, + }; + Containers::ArrayView view = data; + + Containers::ArrayView decorate1 = Implementation::spirvFindInstruction(view, SpvOpDecorate); + CORRADE_COMPARE(decorate1.size(), 4); + CORRADE_COMPARE(decorate1.data(), data + 3); + CORRADE_COMPARE(view.data(), data + 7); + + /* Verify a single-word instruction works too */ + Containers::ArrayView nop = Implementation::spirvFindInstruction(view, SpvOpNop); + CORRADE_COMPARE(nop.size(), 1); + CORRADE_COMPARE(nop.data(), data + 7); + CORRADE_COMPARE(view.data(), data + 8); + + Containers::ArrayView decorate2 = Implementation::spirvFindInstruction(view, SpvOpDecorate); + CORRADE_COMPARE(decorate2.size(), 4); + CORRADE_COMPARE(decorate2.data(), data + 9); + CORRADE_COMPARE(view.data(), data + 13); + + /* We're at the end, there's no more OpDecorate instructions to find */ + CORRADE_VERIFY(!Implementation::spirvFindInstruction(view, SpvOpDecorate)); +} + +void SpirvTest::findInstructionNotEnoughData() { + const UnsignedInt data[] { + op(3, SpvOpMemoryModel), SpvAddressingModelLogical, SpvMemoryModelGLSL450, + /* Should be 4 */ + op(5, SpvOpDecorate), 12, SpvDecorationLocation, 0 + }; + Containers::ArrayView view = data; + + CORRADE_VERIFY(!Implementation::spirvFindInstruction(view, SpvOpDecorate)); + /* View gets set to the first invalid instruction */ + CORRADE_COMPARE(view.data(), data + 3); +} + +void SpirvTest::nextEntrypoint() { + Containers::Array data = Utility::Directory::read(Utility::Directory::join(SHADERTOOLS_TEST_DIR, "SpirvTestFiles/entrypoint-interface.spv")); + + /* The file is a full SPIR-V, strip the header first */ + Containers::ArrayView view = Implementation::spirvData(data, data.size()); + CORRADE_VERIFY(view); + + Containers::Optional vert = Implementation::spirvNextEntrypoint(view); + CORRADE_VERIFY(vert); + /* Verify that long names get recognized properly */ + CORRADE_COMPARE(vert->name, "vertexLongEntrypointName"); + CORRADE_COMPARE(vert->executionModel, SpvExecutionModelVertex); + /* We don't care about the contents, those would change with each assembly + anyway. Verified fully in entrypointInterface(). */ + CORRADE_COMPARE(vert->interfaces.size(), 4); + + Containers::Optional frag = Implementation::spirvNextEntrypoint(view); + CORRADE_VERIFY(frag); + CORRADE_COMPARE(frag->name, "fra"); + CORRADE_COMPARE(frag->executionModel, SpvExecutionModelFragment); + CORRADE_COMPARE(frag->interfaces.size(), 3); + + /* Only two entrypoints in this file */ + CORRADE_VERIFY(!Implementation::spirvNextEntrypoint(view)); +} + +void SpirvTest::nextEntrypointInvalidInstruction() { + const UnsignedInt data[] { + op(3, SpvOpMemoryModel), SpvAddressingModelLogical, SpvMemoryModelGLSL450, + + /* Should be 4 (missing name) */ + op(3, SpvOpEntryPoint), SpvExecutionModelVertex, 1 + }; + Containers::ArrayView view = data; + + CORRADE_VERIFY(!Implementation::spirvNextEntrypoint(view)); +} + +void SpirvTest::entrypointInterface() { + Containers::Array data = Utility::Directory::read(Utility::Directory::join(SHADERTOOLS_TEST_DIR, "SpirvTestFiles/entrypoint-interface.spv")); + + /* The file is a full SPIR-V, strip the header first */ + Containers::ArrayView view = Implementation::spirvData(data, data.size()); + CORRADE_VERIFY(view); + + Containers::Optional vert = Implementation::spirvNextEntrypoint(view); + CORRADE_VERIFY(vert); + CORRADE_COMPARE(vert->interfaces.size(), 4); + + Implementation::SpirvEntrypointInterface vertInterface[4]{}; + Implementation::spirvEntrypointInterface(view, *vert, vertInterface); + CORRADE_VERIFY(vertInterface[0].location); /* position */ + CORRADE_VERIFY(vertInterface[0].storageClass); + CORRADE_COMPARE(*vertInterface[0].location, 0); + CORRADE_COMPARE(*vertInterface[0].storageClass, SpvStorageClassInput); + + CORRADE_VERIFY(vertInterface[1].location); /* color */ + CORRADE_VERIFY(vertInterface[1].storageClass); + CORRADE_COMPARE(*vertInterface[1].location, 1); + CORRADE_COMPARE(*vertInterface[1].storageClass, SpvStorageClassInput); + + /* Verify that absence of location is handled properly */ + CORRADE_VERIFY(!vertInterface[2].location); /* gl_Position */ + CORRADE_VERIFY(vertInterface[2].storageClass); + CORRADE_COMPARE(*vertInterface[2].storageClass, SpvStorageClassOutput); + + CORRADE_VERIFY(vertInterface[3].location); /* interpolatedColorOut */ + CORRADE_VERIFY(vertInterface[2].storageClass); + CORRADE_COMPARE(*vertInterface[3].location, 0); + CORRADE_COMPARE(*vertInterface[2].storageClass, SpvStorageClassOutput); + + Containers::Optional frag = Implementation::spirvNextEntrypoint(view); + CORRADE_VERIFY(frag); + CORRADE_COMPARE(frag->interfaces.size(), 3); + + Implementation::SpirvEntrypointInterface fragInterface[3]{}; + Implementation::spirvEntrypointInterface(view, *frag, fragInterface); + CORRADE_VERIFY(fragInterface[0].location); /* interpolatedColorIn */ + CORRADE_VERIFY(fragInterface[0].storageClass); + CORRADE_COMPARE(*fragInterface[0].location, 0); + CORRADE_COMPARE(*fragInterface[0].storageClass, SpvStorageClassInput); + + CORRADE_VERIFY(fragInterface[1].location); /* fragmentColor */ + CORRADE_VERIFY(fragInterface[1].storageClass); + CORRADE_COMPARE(*fragInterface[1].location, 0); + CORRADE_COMPARE(*fragInterface[1].storageClass, SpvStorageClassOutput); + + /* Verify that absence of storageClass is handled properly */ + CORRADE_VERIFY(fragInterface[2].location); /* unknownFragmentInterface */ + CORRADE_VERIFY(!fragInterface[2].storageClass); + CORRADE_COMPARE(*fragInterface[2].location, 1); +} + +void SpirvTest::entrypointInterfaceNothing() { + const UnsignedInt data[] { + op(3, SpvOpMemoryModel), SpvAddressingModelLogical, SpvMemoryModelGLSL450, + + op(4, SpvOpEntryPoint), SpvExecutionModelGLCompute, 1, '\0' + }; + Containers::ArrayView view = data; + + Containers::Optional comp = Implementation::spirvNextEntrypoint(view); + CORRADE_VERIFY(comp); + CORRADE_VERIFY(comp->interfaces.empty()); + + Implementation::spirvEntrypointInterface(view, *comp, {}); + + /* Well, it shouldn't crash */ + CORRADE_VERIFY(true); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::ShaderTools::Test::SpirvTest) diff --git a/src/Magnum/ShaderTools/Test/SpirvTestFiles/convert.sh b/src/Magnum/ShaderTools/Test/SpirvTestFiles/convert.sh new file mode 100755 index 000000000..0e65958ac --- /dev/null +++ b/src/Magnum/ShaderTools/Test/SpirvTestFiles/convert.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +for i in $(ls *.spvasm); do + magnum-shaderconverter $i ${i%asm} +done diff --git a/src/Magnum/ShaderTools/Test/SpirvTestFiles/entrypoint-interface.spv b/src/Magnum/ShaderTools/Test/SpirvTestFiles/entrypoint-interface.spv new file mode 100644 index 000000000..1b90af3a0 Binary files /dev/null and b/src/Magnum/ShaderTools/Test/SpirvTestFiles/entrypoint-interface.spv differ diff --git a/src/Magnum/ShaderTools/Test/SpirvTestFiles/entrypoint-interface.spvasm b/src/Magnum/ShaderTools/Test/SpirvTestFiles/entrypoint-interface.spvasm new file mode 100644 index 000000000..2bb920daf --- /dev/null +++ b/src/Magnum/ShaderTools/Test/SpirvTestFiles/entrypoint-interface.spvasm @@ -0,0 +1,23 @@ + OpCapability Shader + OpEntryPoint Vertex %ver "vertexLongEntrypointName" %position %color %gl_Position %interpolatedColorOut + OpEntryPoint Fragment %fra "fra" %interpolatedColorIn %fragmentColor %unknownFragmentInterface + OpExecutionMode %fra OriginUpperLeft + OpDecorate %gl_Position BuiltIn Position + OpDecorate %position Location 0 + OpDecorate %color Location 1 + OpDecorate %fragmentColor Location 0 + OpDecorate %unknownFragmentInterface Location 1 + OpDecorate %interpolatedColorIn Location 0 + OpDecorate %interpolatedColorOut Location 0 + %void = OpTypeVoid + %10 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float + %position = OpVariable %_ptr_Input_v4float Input + %color = OpVariable %_ptr_Input_v4float Input +%interpolatedColorIn = OpVariable %_ptr_Input_v4float Input +%_ptr_Output_v4float = OpTypePointer Output %v4float +%gl_Position = OpVariable %_ptr_Output_v4float Output +%interpolatedColorOut = OpVariable %_ptr_Output_v4float Output +%fragmentColor = OpVariable %_ptr_Output_v4float Output