diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3cdefc746..d913ac2f9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -202,25 +202,26 @@ jobs: echo nofiles=ignore>> "%GITHUB_OUTPUT%" echo moredef=-DLOVE_EXTRA_DLLS=%CD%\angle\libEGL.dll;%CD%\angle\libGLESv2.dll>> "%GITHUB_OUTPUT%" exit /b 0 - - name: Download Windows SDK Setup 10.0.20348 - run: curl -Lo winsdksetup.exe https://go.microsoft.com/fwlink/?linkid=2164145 - - name: Install Debugging Tools for Windows - id: windbg + - name: Download pdbstr + run: curl -Lfo pdbstr.nupkg https://www.nuget.org/api/v2/package/Microsoft.Debugging.Tools.PdbStr/20230731.1609.0 + - name: Download srctool + run: curl -Lfo srctool.nupkg https://www.nuget.org/api/v2/package/Microsoft.Debugging.Tools.SrcTool/20230731.1609.0 + - name: Extract Tools and Add to PATH run: | - setlocal enabledelayedexpansion - start /WAIT %CD%\winsdksetup.exe /features OptionId.WindowsDesktopDebuggers /q /log %CD%\log.txt - echo ERRORLEVEL=!ERRORLEVEL! >> %GITHUB_OUTPUT% - - name: Print Debugging Tools Install Log - if: always() - run: | - type log.txt - exit /b ${{ steps.windbg.outputs.ERRORLEVEL }} + mkdir debugtools + cd debugtools + if errorlevel 1 exit /b 1 + 7z e ..\srctool.nupkg content/amd64/srctool.exe + if errorlevel 1 exit /b 1 + 7z e ..\pdbstr.nupkg content/amd64/pdbstr.exe + if errorlevel 1 exit /b 1 + echo %CD%>>%GITHUB_PATH% - name: Setup Python 3.10 uses: actions/setup-python@v5 with: python-version: "3.10" - name: Download source_index.py - run: curl -Lo source_index.py https://gist.github.com/MikuAuahDark/d9c099f5714e09a765496471c2827a55/raw/df34956052035f3473c5f01861dfb53930d06843/source_index.py + run: curl -Lfo source_index.py https://gist.github.com/MikuAuahDark/d9c099f5714e09a765496471c2827a55/raw/df34956052035f3473c5f01861dfb53930d06843/source_index.py - name: Clone Megasource uses: actions/checkout@v4 with: @@ -343,8 +344,9 @@ jobs: # windows opengles tests - name: Run Tests (opengles) if: steps.vars.outputs.arch != 'ARM64' && steps.vars.outputs.compatname != '-compat' + env: + LOVE_GRAPHICS_USE_OPENGLES: 1 run: | - $ENV:LOVE_GRAPHICS_USE_OPENGLES=1 powershell.exe ./install/lovec.exe ./megasource/libs/love/testing/main.lua --runAllTests --isRunner - name: Love Test Report (opengles) id: report2 @@ -481,3 +483,76 @@ jobs: - name: Build run: xcodebuild -project platform/xcode/love.xcodeproj -scheme love-ios -configuration Release -destination 'platform=iOS Simulator,name=iPhone 11' + Android: + runs-on: ubuntu-latest + strategy: + matrix: + build_type: [Debug, Release] + env: + GRADLE_OPTS: "-Dorg.gradle.jvmargs='-Xmx4G'" + steps: + - name: Prepare Environment + run: sudo apt-get update && curl -Lfo kitware-archive.sh https://apt.kitware.com/kitware-archive.sh && sudo bash ./kitware-archive.sh + - name: Install Dependencies + run: sudo apt-get install ninja-build cmake + - name: Checkout love-android + uses: actions/checkout@v4 + with: + repository: love2d/love-android + submodules: false + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: adopt-hotspot + java-version: 17 + cache: gradle + - name: Clone Megasource + uses: actions/checkout@v4 + with: + path: app/src/main/cpp/megasource + repository: love2d/megasource + ref: main + - name: Checkout + uses: actions/checkout@v4 + with: + path: app/src/main/cpp/love + - name: Build Normal Flavor + run: bash ./gradlew assembleNormalRecord${{ matrix.build_type }} + - name: Build Release-specific Binaries + if: ${{ matrix.build_type == 'Release' }} + run: bash ./gradlew bundleNormalNoRecordRelease bundleEmbedRecordRelease bundleEmbedNoRecordRelease + - name: Artifact (Normal debug APK) + if: ${{ matrix.build_type == 'Debug' }} + uses: actions/upload-artifact@v4 + with: + name: love-android-debug.apk + path: app/build/outputs/apk/normalRecord/debug/app-normal-record-debug.apk + - name: Artifact (Normal unsigned APK) + if: ${{ matrix.build_type == 'Release' }} + uses: actions/upload-artifact@v4 + with: + name: love-android.apk + path: app/build/outputs/apk/normalRecord/release/app-normal-record-release-unsigned.apk + - name: Artifact (Normal AAB w/o recording) + if: ${{ matrix.build_type == 'Release' }} + uses: actions/upload-artifact@v4 + with: + name: love-android-ps.aab + path: app/build/outputs/bundle/normalNoRecordRelease/app-normal-noRecord-release.aab + - name: Artifact (Embed AAB) + if: ${{ matrix.build_type == 'Release' }} + uses: actions/upload-artifact@v4 + with: + name: love-android-embed-record.aab + path: app/build/outputs/bundle/embedRecordRelease/app-embed-record-release.aab + - name: Artifact (Embed AAB w/o recording) + if: ${{ matrix.build_type == 'Release' }} + uses: actions/upload-artifact@v4 + with: + name: love-android-embed.aab + path: app/build/outputs/bundle/embedNoRecordRelease/app-embed-noRecord-release.aab + - name: Artifact (Debug symbols) + uses: actions/upload-artifact@v4 + with: + name: love-android-unstripped-debugsyms-${{ matrix.build_type }} + path: app/build/intermediates/cxx diff --git a/src/common/Range.h b/src/common/Range.h index 7daf07af4..14e274f59 100644 --- a/src/common/Range.h +++ b/src/common/Range.h @@ -61,7 +61,7 @@ struct Range return first <= other.first && last >= other.last; } - bool intersects(const Range &other) + bool intersects(const Range &other) const { return !(first > other.last || last < other.first); } diff --git a/src/modules/graphics/Shader.cpp b/src/modules/graphics/Shader.cpp index cfe9e1a6c..d25842252 100644 --- a/src/modules/graphics/Shader.cpp +++ b/src/modules/graphics/Shader.cpp @@ -691,9 +691,48 @@ Shader::Shader(StrongRef _stages[], const CompileOptions &options) , debugName(options.debugName) { std::string err; - if (!validateInternal(_stages, err, validationReflection)) + if (!validateInternal(_stages, err, reflection)) throw love::Exception("%s", err.c_str()); + activeTextures.resize(reflection.textureCount); + activeBuffers.resize(reflection.bufferCount); + + auto gfx = Module::getInstance(Module::M_GRAPHICS); + + // Default bindings for read-only resources. + for (const auto &kvp : reflection.allUniforms) + { + const auto &u = *kvp.second; + + if (u.resourceIndex < 0) + continue; + + if ((u.access & ACCESS_WRITE) != 0) + continue; + + if (u.baseType == UNIFORM_SAMPLER || u.baseType == UNIFORM_STORAGETEXTURE) + { + auto tex = gfx->getDefaultTexture(u.textureType, u.dataBaseType); + for (int i = 0; i < u.count; i++) + { + tex->retain(); + activeTextures[u.resourceIndex + i] = tex; + } + } + else if (u.baseType == UNIFORM_TEXELBUFFER || u.baseType == UNIFORM_STORAGEBUFFER) + { + auto buffer = u.baseType == UNIFORM_TEXELBUFFER + ? gfx->getDefaultTexelBuffer(u.dataBaseType) + : gfx->getDefaultStorageBuffer(); + + for (int i = 0; i < u.count; i++) + { + buffer->retain(); + activeBuffers[u.resourceIndex + i] = buffer; + } + } + } + for (int i = 0; i < SHADERSTAGE_MAX_ENUM; i++) stages[i] = _stages[i]; } @@ -708,6 +747,18 @@ Shader::~Shader() if (current == this) attachDefault(STANDARD_DEFAULT); + + for (Texture *tex : activeTextures) + { + if (tex) + tex->release(); + } + + for (Buffer *buffer : activeBuffers) + { + if (buffer) + buffer->release(); + } } bool Shader::hasStage(ShaderStageType stage) @@ -740,6 +791,18 @@ bool Shader::isDefaultActive() return false; } +const Shader::UniformInfo *Shader::getUniformInfo(const std::string &name) const +{ + const auto it = reflection.allUniforms.find(name); + return it != reflection.allUniforms.end() ? it->second : nullptr; +} + +bool Shader::hasUniform(const std::string &name) const +{ + const auto it = reflection.allUniforms.find(name); + return it != reflection.allUniforms.end() && it->second->active; +} + const Shader::UniformInfo *Shader::getMainTextureInfo() const { return getUniformInfo(BUILTIN_TEXTURE_MAIN); @@ -781,9 +844,9 @@ bool Shader::isResourceBaseTypeCompatible(DataBaseType a, DataBaseType b) void Shader::validateDrawState(PrimitiveType primtype, Texture *maintex) const { - if ((primtype == PRIMITIVE_POINTS) != validationReflection.usesPointSize) + if ((primtype == PRIMITIVE_POINTS) != reflection.usesPointSize) { - if (validationReflection.usesPointSize) + if (reflection.usesPointSize) throw love::Exception("The active shader can only be used to draw points."); else throw love::Exception("The gl_PointSize variable must be set in a vertex shader when drawing points."); @@ -825,17 +888,29 @@ void Shader::validateDrawState(PrimitiveType primtype, Texture *maintex) const void Shader::getLocalThreadgroupSize(int *x, int *y, int *z) { - *x = validationReflection.localThreadgroupSize[0]; - *y = validationReflection.localThreadgroupSize[1]; - *z = validationReflection.localThreadgroupSize[2]; + *x = reflection.localThreadgroupSize[0]; + *y = reflection.localThreadgroupSize[1]; + *z = reflection.localThreadgroupSize[2]; } bool Shader::validate(StrongRef stages[], std::string& err) { - ValidationReflection reflection; + Reflection reflection; return validateInternal(stages, err, reflection); } +static DataBaseType getBaseType(glslang::TBasicType basictype) +{ + switch (basictype) + { + case glslang::EbtInt: return DATA_BASETYPE_INT; + case glslang::EbtUint: return DATA_BASETYPE_UINT; + case glslang::EbtFloat: return DATA_BASETYPE_FLOAT; + case glslang::EbtBool: return DATA_BASETYPE_BOOL; + default: return DATA_BASETYPE_FLOAT; + } +} + static PixelFormat getPixelFormat(glslang::TLayoutFormat format) { using namespace glslang; @@ -885,6 +960,30 @@ static PixelFormat getPixelFormat(glslang::TLayoutFormat format) } } +static TextureType getTextureType(const glslang::TSampler &sampler) +{ + if (sampler.is2D()) + return sampler.isArrayed() ? TEXTURE_2D_ARRAY : TEXTURE_2D; + else if (sampler.dim == glslang::EsdCube) + return sampler.isArrayed() ? TEXTURE_MAX_ENUM : TEXTURE_CUBE; + else if (sampler.dim == glslang::Esd3D) + return TEXTURE_VOLUME; + else + return TEXTURE_MAX_ENUM; +} + +static uint32 getStageMask(EShLanguageMask mask) +{ + uint32 m = 0; + if (mask & EShLangVertexMask) + m |= SHADERSTAGEMASK_VERTEX; + if (mask & EShLangFragmentMask) + m |= SHADERSTAGEMASK_PIXEL; + if (mask & EShLangComputeMask) + m |= SHADERSTAGEMASK_COMPUTE; + return m; +} + template static T convertData(const glslang::TConstUnion &data) { @@ -903,7 +1002,7 @@ static T convertData(const glslang::TConstUnion &data) } } -bool Shader::validateInternal(StrongRef stages[], std::string &err, ValidationReflection &reflection) +bool Shader::validateInternal(StrongRef stages[], std::string &err, Reflection &reflection) { glslang::TProgram program; @@ -946,6 +1045,9 @@ bool Shader::validateInternal(StrongRef stages[], std::string &err, } } + reflection.textureCount = 0; + reflection.bufferCount = 0; + for (int i = 0; i < program.getNumUniformVariables(); i++) { const glslang::TObjectReflection &info = program.getUniform(i); @@ -955,9 +1057,40 @@ bool Shader::validateInternal(StrongRef stages[], std::string &err, const glslang::TQualifier &qualifiers = type->getQualifier(); - if (type->isImage()) + UniformInfo u = {}; + + u.name = canonicaliizeUniformName(info.name); + u.location = -1; + u.access = ACCESS_READ; + u.stageMask = getStageMask(info.stages); + u.components = 1; + u.resourceIndex = -1; + + if (type->isSizedArray()) + u.count = type->getArraySizes()->getCumulativeSize(); + else + u.count = 1; + + const auto &sampler = type->getSampler(); + + if (type->isTexture() && type->getSampler().isCombined()) { - if ((info.stages & EShLangComputeMask) == 0) + u.baseType = UNIFORM_SAMPLER; + u.dataBaseType = getBaseType(sampler.getBasicType()); + u.isDepthSampler = sampler.isShadow(); + u.textureType = getTextureType(sampler); + + if (u.textureType == TEXTURE_MAX_ENUM) + continue; + + u.resourceIndex = reflection.textureCount; + reflection.textureCount += u.count; + + reflection.sampledTextures[u.name] = u; + } + else if (type->isImage()) + { + if ((info.stages & (~EShLangComputeMask)) != 0) { err = "Shader validation error:\nStorage Texture uniform variables (image2D, etc) are only allowed in compute shaders."; return false; @@ -965,36 +1098,63 @@ bool Shader::validateInternal(StrongRef stages[], std::string &err, if (!qualifiers.hasFormat()) { - err = "Shader validation error:\nStorage Texture '" + info.name + "' must have an explicit format set in its layout declaration."; + err = "Shader validation error:\nStorage Texture '" + u.name + "' must have an explicit format set in its layout declaration."; return false; } - StorageTextureReflection texreflection = {}; + u.baseType = UNIFORM_STORAGETEXTURE; + u.storageTextureFormat = getPixelFormat(qualifiers.getFormat()); + u.dataBaseType = getDataBaseType(u.storageTextureFormat); + u.textureType = getTextureType(sampler); - texreflection.format = getPixelFormat(qualifiers.getFormat()); + if (u.textureType == TEXTURE_MAX_ENUM) + continue; + + u.resourceIndex = reflection.textureCount; + reflection.textureCount += u.count; if (qualifiers.isReadOnly()) - texreflection.access = ACCESS_READ; + u.access = ACCESS_READ; else if (qualifiers.isWriteOnly()) - texreflection.access = ACCESS_WRITE; + u.access = ACCESS_WRITE; else - texreflection.access = (Access)(ACCESS_READ | ACCESS_WRITE); + u.access = (Access)(ACCESS_READ | ACCESS_WRITE); + + reflection.storageTextures[u.name] = u; + } + else if (type->getBasicType() == glslang::EbtSampler && type->getSampler().isBuffer()) + { + u.baseType = UNIFORM_TEXELBUFFER; + u.dataBaseType = getBaseType(sampler.getBasicType()); + + u.resourceIndex = reflection.bufferCount; + reflection.bufferCount += u.count; - reflection.storageTextures[info.name] = texreflection; + reflection.texelBuffers[u.name] = u; } else if (!type->isOpaque()) { - LocalUniform u = {}; - auto &values = u.initializerValues; + std::vector values; const glslang::TConstUnionArray *constarray = info.getConstArray(); + if (type->isMatrix()) + { + u.matrix.rows = type->getMatrixRows(); + u.matrix.columns = type->getMatrixCols(); + } + else + { + u.components = type->getVectorSize(); + } + // Store initializer values for local uniforms. Some love graphics // backends strip these out of the shader so we need to be able to // access them (to re-send them) by getting them here. switch (type->getBasicType()) { case glslang::EbtFloat: - u.dataType = DATA_BASETYPE_FLOAT; + u.baseType = type->isMatrix() ? UNIFORM_MATRIX : UNIFORM_FLOAT; + u.dataBaseType = DATA_BASETYPE_FLOAT; if (constarray != nullptr) { values.resize(constarray->size()); @@ -1003,7 +1163,8 @@ bool Shader::validateInternal(StrongRef stages[], std::string &err, } break; case glslang::EbtUint: - u.dataType = DATA_BASETYPE_UINT; + u.baseType = UNIFORM_UINT; + u.dataBaseType = DATA_BASETYPE_UINT; if (constarray != nullptr) { values.resize(constarray->size()); @@ -1012,7 +1173,8 @@ bool Shader::validateInternal(StrongRef stages[], std::string &err, } break; case glslang::EbtBool: - u.dataType = DATA_BASETYPE_BOOL; + u.baseType = UNIFORM_BOOL; + u.dataBaseType = DATA_BASETYPE_BOOL; if (constarray != nullptr) { values.resize(constarray->size()); @@ -1022,7 +1184,8 @@ bool Shader::validateInternal(StrongRef stages[], std::string &err, break; case glslang::EbtInt: default: - u.dataType = DATA_BASETYPE_INT; + u.baseType = UNIFORM_INT; + u.dataBaseType = DATA_BASETYPE_INT; if (constarray != nullptr) { values.resize(constarray->size()); @@ -1032,7 +1195,8 @@ bool Shader::validateInternal(StrongRef stages[], std::string &err, break; } - reflection.localUniforms[info.name] = u; + reflection.localUniforms[u.name] = u; + reflection.localUniformInitializerValues[u.name] = values; } } @@ -1044,7 +1208,7 @@ bool Shader::validateInternal(StrongRef stages[], std::string &err, { const glslang::TQualifier &qualifiers = type->getQualifier(); - if ((!qualifiers.isReadOnly() || qualifiers.isWriteOnly()) && (info.stages & EShLangComputeMask) == 0) + if ((!qualifiers.isReadOnly() || qualifiers.isWriteOnly()) && ((info.stages & (~EShLangComputeMask)) != 0)) { err = "Shader validation error:\nStorage Buffer block '" + info.name + "' must be marked as readonly in vertex and pixel shaders."; return false; @@ -1070,18 +1234,32 @@ bool Shader::validateInternal(StrongRef stages[], std::string &err, return false; } - BufferReflection bufferReflection = {}; - bufferReflection.stride = (size_t) info.size; - bufferReflection.memberCount = (size_t) info.numMembers; + UniformInfo u = {}; + u.name = canonicaliizeUniformName(info.name); + u.location = -1; + u.stageMask = getStageMask(info.stages); + u.components = 1; + u.baseType = UNIFORM_STORAGEBUFFER; + + if (type->isSizedArray()) + u.count = type->getArraySizes()->getCumulativeSize(); + else + u.count = 1; + + u.bufferStride = (size_t) info.size; + u.bufferMemberCount = (size_t) info.numMembers; + + u.resourceIndex = reflection.bufferCount; + reflection.bufferCount += u.count; if (qualifiers.isReadOnly()) - bufferReflection.access = ACCESS_READ; + u.access = ACCESS_READ; else if (qualifiers.isWriteOnly()) - bufferReflection.access = ACCESS_WRITE; + u.access = ACCESS_WRITE; else - bufferReflection.access = (Access)(ACCESS_READ | ACCESS_WRITE); + u.access = (Access)(ACCESS_READ | ACCESS_WRITE); - reflection.storageBuffers[info.name] = bufferReflection; + reflection.storageBuffers[u.name] = u; } else { @@ -1090,6 +1268,21 @@ bool Shader::validateInternal(StrongRef stages[], std::string &err, } } + for (auto &kvp : reflection.texelBuffers) + reflection.allUniforms[kvp.first] = &kvp.second; + + for (auto &kvp : reflection.storageBuffers) + reflection.allUniforms[kvp.first] = &kvp.second; + + for (auto &kvp : reflection.sampledTextures) + reflection.allUniforms[kvp.first] = &kvp.second; + + for (auto &kvp : reflection.storageTextures) + reflection.allUniforms[kvp.first] = &kvp.second; + + for (auto &kvp : reflection.localUniforms) + reflection.allUniforms[kvp.first] = &kvp.second; + return true; } @@ -1216,55 +1409,40 @@ bool Shader::validateBuffer(const UniformInfo *info, Buffer *buffer, bool intern return true; } -bool Shader::fillUniformReflectionData(UniformInfo &u) +std::string Shader::getShaderStageDebugName(ShaderStageType stage) const { - const auto &r = validationReflection; - - if (u.baseType == UNIFORM_STORAGETEXTURE) - { - const auto reflectionit = r.storageTextures.find(u.name); - if (reflectionit != r.storageTextures.end()) - { - u.storageTextureFormat = reflectionit->second.format; - u.access = reflectionit->second.access; - return true; - } + std::string name = debugName; - // No reflection info - maybe glslang was better at detecting dead code - // than the driver's compiler? - return false; - } - else if (u.baseType == UNIFORM_STORAGEBUFFER) + if (!name.empty()) { - const auto reflectionit = r.storageBuffers.find(u.name); - if (reflectionit != r.storageBuffers.end()) - { - u.bufferStride = reflectionit->second.stride; - u.bufferMemberCount = reflectionit->second.memberCount; - u.access = reflectionit->second.access; - return true; - } - - return false; + const char *stagename = "unknown"; + ShaderStage::getConstant(stage, stagename); + name += " (" + std::string(stagename) + ")"; } - return true; + return name; } -std::string Shader::getShaderStageDebugName(ShaderStageType stage) const +std::string Shader::canonicaliizeUniformName(const std::string &n) { - std::string name = debugName; + std::string name(n); - if (!name.empty()) + // Some drivers/compilers append "[0]" to the end of array uniform names. + if (name.length() > 3) { - const char *stagename = "unknown"; - ShaderStage::getConstant(stage, stagename); - name += " (" + std::string(stagename) + ")"; + size_t findpos = name.rfind("[0]"); + if (findpos != std::string::npos && findpos == name.length() - 3) + name.erase(name.length() - 3); } return name; } +void Shader::handleUnknownUniformName(const char */*name*/) +{ + // TODO: do something here? +} + bool Shader::initialize() { return glslang::InitializeProcess(); diff --git a/src/modules/graphics/Shader.h b/src/modules/graphics/Shader.h index d9e53999b..51b538fd7 100644 --- a/src/modules/graphics/Shader.h +++ b/src/modules/graphics/Shader.h @@ -129,6 +129,10 @@ class Shader : public Object, public Resource struct UniformInfo { + UniformType baseType; + uint32 stageMask; + bool active; + int location; int count; @@ -138,7 +142,6 @@ class Shader : public Object, public Resource MatrixSize matrix; }; - UniformType baseType; DataBaseType dataBaseType; TextureType textureType; Access access; @@ -148,6 +151,8 @@ class Shader : public Object, public Resource size_t bufferMemberCount; std::string name; + int resourceIndex; + union { void *data; @@ -157,12 +162,6 @@ class Shader : public Object, public Resource }; size_t dataSize; - - union - { - Texture **textures; - Buffer **buffers; - }; }; union LocalUniformValue @@ -222,7 +221,7 @@ class Shader : public Object, public Resource virtual int getVertexAttributeIndex(const std::string &name) = 0; - virtual const UniformInfo *getUniformInfo(const std::string &name) const = 0; + const UniformInfo *getUniformInfo(const std::string &name) const; virtual const UniformInfo *getUniformInfo(BuiltinUniform builtin) const = 0; virtual void updateUniform(const UniformInfo *info, int count) = 0; @@ -234,7 +233,7 @@ class Shader : public Object, public Resource * Gets whether a uniform with the specified name exists and is actively * used in the shader. **/ - virtual bool hasUniform(const std::string &name) const = 0; + bool hasUniform(const std::string &name) const; /** * Sets the textures used when rendering a video. For internal use only. @@ -264,39 +263,31 @@ class Shader : public Object, public Resource protected: - struct BufferReflection + struct Reflection { - size_t stride; - size_t memberCount; - Access access; - }; + std::map texelBuffers; + std::map storageBuffers; + std::map sampledTextures; + std::map storageTextures; + std::map localUniforms; - struct StorageTextureReflection - { - PixelFormat format; - Access access; - }; + std::map allUniforms; - struct LocalUniform - { - DataBaseType dataType; - std::vector initializerValues; - }; + std::map> localUniformInitializerValues; + + int textureCount; + int bufferCount; - struct ValidationReflection - { - std::map storageBuffers; - std::map storageTextures; - std::map localUniforms; int localThreadgroupSize[3]; bool usesPointSize; }; - bool fillUniformReflectionData(UniformInfo &u); - std::string getShaderStageDebugName(ShaderStageType stage) const; - static bool validateInternal(StrongRef stages[], std::string& err, ValidationReflection &reflection); + void handleUnknownUniformName(const char *name); + + static std::string canonicaliizeUniformName(const std::string &name); + static bool validateInternal(StrongRef stages[], std::string& err, Reflection &reflection); static DataBaseType getDataBaseType(PixelFormat format); static bool isResourceBaseTypeCompatible(DataBaseType a, DataBaseType b); @@ -305,7 +296,10 @@ class Shader : public Object, public Resource StrongRef stages[SHADERSTAGE_MAX_ENUM]; - ValidationReflection validationReflection; + Reflection reflection; + + std::vector activeTextures; + std::vector activeBuffers; std::string debugName; diff --git a/src/modules/graphics/ShaderStage.h b/src/modules/graphics/ShaderStage.h index 69214a3a8..fda4e5b9c 100644 --- a/src/modules/graphics/ShaderStage.h +++ b/src/modules/graphics/ShaderStage.h @@ -48,6 +48,14 @@ enum ShaderStageType SHADERSTAGE_MAX_ENUM }; +enum ShaderStageMask +{ + SHADERSTAGEMASK_NONE = 0, + SHADERSTAGEMASK_VERTEX = 1 << SHADERSTAGE_VERTEX, + SHADERSTAGEMASK_PIXEL = 1 << SHADERSTAGE_PIXEL, + SHADERSTAGEMASK_COMPUTE = 1 << SHADERSTAGE_COMPUTE, +}; + class ShaderStage : public love::Object { public: diff --git a/src/modules/graphics/StreamBuffer.h b/src/modules/graphics/StreamBuffer.h index d016ce5bf..ceaa87f94 100644 --- a/src/modules/graphics/StreamBuffer.h +++ b/src/modules/graphics/StreamBuffer.h @@ -57,6 +57,8 @@ class StreamBuffer : public love::Object, public Resource BufferUsage getMode() const { return mode; } size_t getUsableSize() const { return bufferSize - frameGPUReadOffset; } + virtual size_t getGPUReadOffset() const = 0; + virtual MapInfo map(size_t minsize) = 0; virtual size_t unmap(size_t usedsize) = 0; virtual void markUsed(size_t usedsize) = 0; diff --git a/src/modules/graphics/Texture.cpp b/src/modules/graphics/Texture.cpp index a880a51e8..edae44ff7 100644 --- a/src/modules/graphics/Texture.cpp +++ b/src/modules/graphics/Texture.cpp @@ -166,8 +166,8 @@ Texture::Texture(Graphics *gfx, const Settings &settings, const Slices *slices) , format(settings.format) , renderTarget(settings.renderTarget) , computeWrite(settings.computeWrite) - , viewFormats(settings.viewFormats) , readable(true) + , viewFormats(settings.viewFormats) , mipmapsMode(settings.mipmaps) , width(settings.width) , height(settings.height) @@ -336,8 +336,8 @@ Texture::Texture(Graphics *gfx, Texture *base, const ViewSettings &viewsettings) , format(viewsettings.format.get(base->getPixelFormat())) , renderTarget(base->renderTarget) , computeWrite(base->computeWrite) - , viewFormats(base->viewFormats) , readable(base->readable) + , viewFormats(base->viewFormats) , mipmapsMode(base->mipmapsMode) , width(1) , height(1) @@ -354,7 +354,7 @@ Texture::Texture(Graphics *gfx, Texture *base, const ViewSettings &viewsettings) , rootView({base->rootView.texture, 0, 0}) , parentView({base, viewsettings.mipmapStart.get(0), viewsettings.layerStart.get(0)}) { - width = base->getHeight(parentView.startMipmap); + width = base->getWidth(parentView.startMipmap); height = base->getHeight(parentView.startMipmap); if (texType == TEXTURE_VOLUME) diff --git a/src/modules/graphics/metal/Graphics.h b/src/modules/graphics/metal/Graphics.h index 4a7ebf219..41aa71be9 100644 --- a/src/modules/graphics/metal/Graphics.h +++ b/src/modules/graphics/metal/Graphics.h @@ -242,6 +242,7 @@ class Graphics final : public love::graphics::Graphics StreamBuffer *uniformBuffer; StreamBuffer::MapInfo uniformBufferData; size_t uniformBufferOffset; + size_t uniformBufferGPUStart; Buffer *defaultAttributesBuffer; diff --git a/src/modules/graphics/metal/Graphics.mm b/src/modules/graphics/metal/Graphics.mm index e4e9dc2af..ecad1ea24 100644 --- a/src/modules/graphics/metal/Graphics.mm +++ b/src/modules/graphics/metal/Graphics.mm @@ -280,6 +280,7 @@ static inline void setSampler(id encoder, Graphics::Re , attachmentStoreActions() , renderBindings() , uniformBufferOffset(0) + , uniformBufferGPUStart(0) , defaultAttributesBuffer(nullptr) , families() { @autoreleasepool { @@ -341,6 +342,7 @@ static inline void setSampler(id encoder, Graphics::Re }; Buffer::Settings attribsettings(BUFFERUSAGEFLAG_VERTEX, BUFFERDATAUSAGE_STATIC); + attribsettings.debugName = "Default Vertex Attributes"; defaultAttributesBuffer = newBuffer(attribsettings, dataformat, &defaults, sizeof(DefaultVertexAttributes), 0); } @@ -1034,14 +1036,19 @@ static bool isClampOne(SamplerState::WrapMode w) if (uniformBuffer->getSize() < uniformBufferOffset + size) { size_t newsize = uniformBuffer->getSize() * 2; + if (uniformBufferOffset > 0) + uniformBuffer->nextFrame(); uniformBuffer->release(); - uniformBuffer = CreateStreamBuffer(device, BUFFERUSAGE_VERTEX, newsize); + uniformBuffer = CreateStreamBuffer(device, BUFFERUSAGE_UNIFORM, newsize); uniformBufferData = {}; uniformBufferOffset = 0; } if (uniformBufferData.data == nullptr) + { uniformBufferData = uniformBuffer->map(uniformBuffer->getSize()); + uniformBufferGPUStart = uniformBuffer->getGPUReadOffset(); + } memcpy(uniformBufferData.data + uniformBufferOffset, bufferdata, size); @@ -1049,7 +1056,7 @@ static bool isClampOne(SamplerState::WrapMode w) int uniformindex = Shader::getUniformBufferBinding(); auto &bindings = renderBindings; - setBuffer(encoder, bindings, uniformindex, buffer, uniformBufferOffset); + setBuffer(encoder, bindings, uniformindex, buffer, uniformBufferGPUStart + uniformBufferOffset); uniformBufferOffset += alignUp(size, alignment); @@ -1140,14 +1147,19 @@ static bool isClampOne(SamplerState::WrapMode w) if (uniformBuffer->getSize() < uniformBufferOffset + size) { size_t newsize = uniformBuffer->getSize() * 2; + if (uniformBufferOffset > 0) + uniformBuffer->nextFrame(); uniformBuffer->release(); - uniformBuffer = CreateStreamBuffer(device, BUFFERUSAGE_VERTEX, newsize); + uniformBuffer = CreateStreamBuffer(device, BUFFERUSAGE_UNIFORM, newsize); uniformBufferData = {}; uniformBufferOffset = 0; } if (uniformBufferData.data == nullptr) + { uniformBufferData = uniformBuffer->map(uniformBuffer->getSize()); + uniformBufferGPUStart = uniformBuffer->getGPUReadOffset(); + } memcpy(uniformBufferData.data + uniformBufferOffset, bufferdata, size); @@ -1155,8 +1167,8 @@ static bool isClampOne(SamplerState::WrapMode w) int uniformindex = Shader::getUniformBufferBinding(); auto &bindings = renderBindings; - setBuffer(renderEncoder, bindings, SHADERSTAGE_VERTEX, uniformindex, buffer, uniformBufferOffset); - setBuffer(renderEncoder, bindings, SHADERSTAGE_PIXEL, uniformindex, buffer, uniformBufferOffset); + setBuffer(renderEncoder, bindings, SHADERSTAGE_VERTEX, uniformindex, buffer, uniformBufferGPUStart + uniformBufferOffset); + setBuffer(renderEncoder, bindings, SHADERSTAGE_PIXEL, uniformindex, buffer, uniformBufferGPUStart + uniformBufferOffset); uniformBufferOffset += alignUp(size, alignment); diff --git a/src/modules/graphics/metal/Shader.h b/src/modules/graphics/metal/Shader.h index 1e517a950..174902bfc 100644 --- a/src/modules/graphics/metal/Shader.h +++ b/src/modules/graphics/metal/Shader.h @@ -107,12 +107,10 @@ class Shader final : public love::graphics::Shader void attach() override; std::string getWarnings() const override { return ""; } int getVertexAttributeIndex(const std::string &name) override; - const UniformInfo *getUniformInfo(const std::string &name) const override; const UniformInfo *getUniformInfo(BuiltinUniform builtin) const override; void updateUniform(const UniformInfo *info, int count) override; void sendTextures(const UniformInfo *info, love::graphics::Texture **textures, int count) override; void sendBuffers(const UniformInfo *info, love::graphics::Buffer **buffers, int count) override; - bool hasUniform(const std::string &name) const override; ptrdiff_t getHandle() const override { return 0; } void setVideoTextures(love::graphics::Texture *ytexture, love::graphics::Texture *cbtexture, love::graphics::Texture *crtexture) override; @@ -140,13 +138,11 @@ class Shader final : public love::graphics::Shader }; void buildLocalUniforms(const spirv_cross::CompilerMSL &msl, const spirv_cross::SPIRType &type, size_t baseoffset, const std::string &basename); - void addImage(const spirv_cross::CompilerMSL &msl, const spirv_cross::Resource &resource, UniformType baseType); void compileFromGLSLang(id device, const glslang::TProgram &program); id functions[SHADERSTAGE_MAX_ENUM]; UniformInfo *builtinUniformInfo[BUILTIN_MAX_ENUM]; - std::map uniforms; uint8 *localUniformStagingData; uint8 *localUniformBufferData; diff --git a/src/modules/graphics/metal/Shader.mm b/src/modules/graphics/metal/Shader.mm index 5ed42deb2..e2859dd55 100644 --- a/src/modules/graphics/metal/Shader.mm +++ b/src/modules/graphics/metal/Shader.mm @@ -299,134 +299,47 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) continue; } + name = canonicaliizeUniformName(name); + if (offset + membersize > localUniformBufferSize) throw love::Exception("Invalid uniform offset + size for '%s' (offset=%d, size=%d, buffer size=%d)", name.c_str(), (int)offset, (int)membersize, (int)localUniformBufferSize); - UniformInfo u = {}; - u.name = name; - u.dataSize = membersize; - u.count = membertype.array.empty() ? 1 : membertype.array[0]; - u.components = 1; - - u.data = localUniformStagingData + offset; - if (membertype.columns == 1) + auto uniformit = reflection.allUniforms.find(name); + if (uniformit == reflection.allUniforms.end()) { - if (membertype.basetype == SPIRType::Int) - u.baseType = UNIFORM_INT; - else if (membertype.basetype == SPIRType::UInt) - u.baseType = UNIFORM_UINT; - else - u.baseType = UNIFORM_FLOAT; - u.components = membertype.vecsize; - } - else - { - u.baseType = UNIFORM_MATRIX; - u.matrix.rows = membertype.vecsize; - u.matrix.columns = membertype.columns; + handleUnknownUniformName(name.c_str()); + continue; } - const auto &reflectionit = validationReflection.localUniforms.find(u.name); - if (reflectionit != validationReflection.localUniforms.end()) + UniformInfo &u = *(uniformit->second); + u.active = true; + + if (u.dataSize > 0) + continue; + + u.dataSize = membersize; + u.data = localUniformStagingData + offset; + + const auto &reflectionit = reflection.localUniformInitializerValues.find(u.name); + if (reflectionit != reflection.localUniformInitializerValues.end()) { - const auto &localuniform = reflectionit->second; - const auto &values = localuniform.initializerValues; + const auto &values = reflectionit->second; if (!values.empty()) memcpy(u.data, values.data(), std::min(u.dataSize, values.size() * sizeof(LocalUniformValue))); } - uniforms[u.name] = u; - BuiltinUniform builtin = BUILTIN_MAX_ENUM; if (getConstant(u.name.c_str(), builtin)) { if (builtin == BUILTIN_UNIFORMS_PER_DRAW) builtinUniformDataOffset = offset; - builtinUniformInfo[builtin] = &uniforms[u.name]; + builtinUniformInfo[builtin] = &u; } updateUniform(&u, u.count); } } -void Shader::addImage(const spirv_cross::CompilerMSL &msl, const spirv_cross::Resource &resource, UniformType baseType) -{ - using namespace spirv_cross; - - const SPIRType &basetype = msl.get_type(resource.base_type_id); - const SPIRType &type = msl.get_type(resource.type_id); - const SPIRType &imagetype = msl.get_type(basetype.image.type); - - UniformInfo u = {}; - u.baseType = baseType; - u.name = resource.name; - u.count = type.array.empty() ? 1 : type.array[0]; - u.isDepthSampler = type.image.depth; - u.components = 1; - - auto it = uniforms.find(u.name); - if (it != uniforms.end()) - return; - - if (!fillUniformReflectionData(u)) - return; - - switch (imagetype.basetype) - { - case SPIRType::Float: - u.dataBaseType = DATA_BASETYPE_FLOAT; - break; - case SPIRType::Int: - u.dataBaseType = DATA_BASETYPE_INT; - break; - case SPIRType::UInt: - u.dataBaseType = DATA_BASETYPE_UINT; - break; - default: - break; - } - - switch (basetype.image.dim) - { - case spv::Dim2D: - u.textureType = basetype.image.arrayed ? TEXTURE_2D_ARRAY : TEXTURE_2D; - u.textures = new love::graphics::Texture*[u.count]; - memset(u.textures, 0, sizeof(love::graphics::Texture *) * u.count); - break; - case spv::Dim3D: - u.textureType = TEXTURE_VOLUME; - u.textures = new love::graphics::Texture*[u.count]; - memset(u.textures, 0, sizeof(love::graphics::Texture *) * u.count); - break; - case spv::DimCube: - if (basetype.image.arrayed) - throw love::Exception("Cubemap Arrays are not currently supported."); - u.textureType = TEXTURE_CUBE; - u.textures = new love::graphics::Texture*[u.count]; - memset(u.textures, 0, sizeof(love::graphics::Texture *) * u.count); - break; - case spv::DimBuffer: - u.baseType = UNIFORM_TEXELBUFFER; - u.buffers = new love::graphics::Buffer*[u.count]; - memset(u.buffers, 0, sizeof(love::graphics::Buffer *) * u.count); - break; - default: - // TODO: error? continue? - break; - } - - u.dataSize = sizeof(int) * u.count; - u.data = malloc(u.dataSize); - for (int i = 0; i < u.count; i++) - u.ints[i] = -1; // Initialized below, after compiling. - - uniforms[u.name] = u; - - BuiltinUniform builtin; - if (getConstant(resource.name.c_str(), builtin)) - builtinUniformInfo[builtin] = &uniforms[u.name]; -} - void Shader::compileFromGLSLang(id device, const glslang::TProgram &program) { using namespace glslang; @@ -473,21 +386,10 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) auto &msl = *mslpointer; auto interfacevars = msl.get_active_interface_variables(); + ShaderResources resources = msl.get_shader_resources(interfacevars); msl.set_enabled_interface_variables(interfacevars); - ShaderResources resources = msl.get_shader_resources(); - - for (const auto &resource : resources.storage_images) - { - addImage(msl, resource, UNIFORM_STORAGETEXTURE); - } - - for (const auto &resource : resources.sampled_images) - { - addImage(msl, resource, UNIFORM_SAMPLER); - } - for (const auto &resource : resources.uniform_buffers) { MSLResourceBinding binding; @@ -535,33 +437,6 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) binding.desc_set = msl.get_decoration(resource.id, spv::DecorationDescriptorSet); binding.msl_buffer = metalBufferIndices[stageindex]++; msl.add_msl_resource_binding(binding); - - auto it = uniforms.find(resource.name); - if (it != uniforms.end()) - continue; - - const SPIRType &type = msl.get_type(resource.type_id); - - UniformInfo u = {}; - u.baseType = UNIFORM_STORAGEBUFFER; - u.components = 1; - u.name = resource.name; - u.count = type.array.empty() ? 1 : type.array[0]; - - if (!fillUniformReflectionData(u)) - continue; - - u.buffers = new love::graphics::Buffer*[u.count]; - u.dataSize = sizeof(int) * u.count; - u.data = malloc(u.dataSize); - - for (int i = 0; i < u.count; i++) - { - u.ints[i] = -1; // Initialized below, after compiling. - u.buffers[i] = nullptr; - } - - uniforms[u.name] = u; } if (stageindex == SHADERSTAGE_VERTEX) @@ -634,7 +509,8 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) if (library == nil && err != nil) { NSLog(@"errors: %@", err); - throw love::Exception("Error compiling converted Metal shader code"); + NSString *errorstr = err.localizedDescription; + throw love::Exception("Error compiling converted Metal shader code:\n\n%s", errorstr.UTF8String); } functions[stageindex] = [library newFunctionWithName:library.functionNames[0]]; @@ -645,11 +521,15 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) auto setTextureBinding = [this](CompilerMSL &msl, int stageindex, const spirv_cross::Resource &resource) -> void { - auto it = uniforms.find(resource.name); - if (it == uniforms.end()) + std::string name = canonicaliizeUniformName(resource.name); + auto it = reflection.allUniforms.find(name); + if (it == reflection.allUniforms.end()) + { + handleUnknownUniformName(name.c_str()); return; + } - UniformInfo &u = it->second; + UniformInfo &u = *(it->second); uint32 texturebinding = msl.get_automatic_msl_resource_binding(resource.id); uint32 samplerbinding = msl.get_automatic_msl_resource_binding_secondary(resource.id); @@ -657,15 +537,16 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) if (texturebinding == (uint32)-1) { // No valid binding, the uniform was likely optimized out because it's not used. - uniforms.erase(resource.name); return; } - for (int i = 0; i < u.count; i++) + u.active = true; + + if (u.location < 0) { - if (u.ints[i] == -1) + u.location = (int)textureBindings.size(); + for (int i = 0; i < u.count; i++) { - u.ints[i] = (int)textureBindings.size(); TextureBinding b = {}; b.access = u.access; @@ -683,11 +564,18 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) textureBindings.push_back(b); } + } - auto &b = textureBindings[u.ints[i]]; + for (int i = 0; i < u.count; i++) + { + auto &b = textureBindings[u.location + i]; b.textureStages[stageindex] = (uint8) texturebinding; b.samplerStages[stageindex] = (uint8) samplerbinding; } + + BuiltinUniform builtin; + if (getConstant(name.c_str(), builtin)) + builtinUniformInfo[builtin] = &u; }; for (const auto &resource : resources.sampled_images) @@ -702,9 +590,13 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) for (const auto &resource : resources.storage_buffers) { - auto it = uniforms.find(resource.name); - if (it == uniforms.end()) + std::string name = canonicaliizeUniformName(resource.name); + auto it = reflection.storageBuffers.find(name); + if (it == reflection.storageBuffers.end()) + { + handleUnknownUniformName(name.c_str()); continue; + } UniformInfo &u = it->second; @@ -712,15 +604,16 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) if (bufferbinding == (uint32)-1) { // No valid binding, the uniform was likely optimized out because it's not used. - uniforms.erase(resource.name); continue; } - for (int i = 0; i < u.count; i++) + u.active = true; + + if (u.location < 0) { - if (u.ints[i] == -1) + u.location = (int)bufferBindings.size(); + for (int i = 0; i < u.count; i++) { - u.ints[i] = (int)bufferBindings.size(); BufferBinding b = {}; b.access = u.access; @@ -729,25 +622,26 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) bufferBindings.push_back(b); } - - bufferBindings[u.ints[i]].stages[stageindex] = (uint8) bufferbinding; } + + for (int i = 0; i < u.count; i++) + bufferBindings[u.location + i].stages[stageindex] = (uint8) bufferbinding; } } // Initialize default resource bindings. - for (auto &kvp : uniforms) + for (const auto &kvp : reflection.allUniforms) { - UniformInfo &info = kvp.second; - switch (info.baseType) + const UniformInfo *info = kvp.second; + switch (info->baseType) { case UNIFORM_SAMPLER: case UNIFORM_STORAGETEXTURE: - sendTextures(&info, info.textures, info.count); + sendTextures(info, &activeTextures[info->resourceIndex], info->count); break; case UNIFORM_TEXELBUFFER: case UNIFORM_STORAGEBUFFER: - sendBuffers(&info, info.buffers, info.count); + sendBuffers(info, &activeBuffers[info->resourceIndex], info->count); break; default: break; @@ -767,31 +661,6 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) cachedRenderPipelines.clear(); - for (const auto &it : uniforms) - { - const auto &u = it.second; - if (u.baseType == UNIFORM_SAMPLER || u.baseType == UNIFORM_STORAGETEXTURE) - { - free(u.data); - for (int i = 0; i < u.count; i++) - { - if (u.textures[i] != nullptr) - u.textures[i]->release(); - } - delete[] u.textures; - } - else if (u.baseType == UNIFORM_TEXELBUFFER || u.baseType == UNIFORM_STORAGEBUFFER) - { - free(u.data); - for (int i = 0; i < u.count; i++) - { - if (u.buffers[i] != nullptr) - u.buffers[i]->release(); - } - delete[] u.buffers; - } - } - delete[] localUniformStagingData; delete[] localUniformBufferData; }} @@ -813,12 +682,6 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) return it != attributes.end() ? it->second : -1; } -const Shader::UniformInfo *Shader::getUniformInfo(const std::string &name) const -{ - const auto it = uniforms.find(name); - return it != uniforms.end() ? &(it->second) : nullptr; -} - const Shader::UniformInfo *Shader::getUniformInfo(BuiltinUniform builtin) const { return builtinUniformInfo[(int)builtin]; @@ -826,6 +689,9 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) void Shader::updateUniform(const UniformInfo *info, int count) { + if (info->dataSize == 0) + return; + if (current == this) Graphics::flushBatchedDrawsGlobal(); @@ -895,12 +761,21 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) tex->retain(); - if (info->textures[i] != nullptr) - info->textures[i]->release(); + int resourceindex = info->resourceIndex + i; + + if (activeTextures[resourceindex] != nullptr) + activeTextures[resourceindex]->release(); + + activeTextures[resourceindex] = tex; - info->textures[i] = tex; + if (info->location < 0) + continue; + + int bindingindex = info->location + i; + if (bindingindex < 0) + continue; - auto &binding = textureBindings[info->ints[i]]; + auto &binding = textureBindings[bindingindex]; if (isdefault && (binding.access & ACCESS_WRITE) != 0) { binding.texture = nil; @@ -927,7 +802,6 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) count = std::min(count, info->count); - // Bind the textures to the texture units. for (int i = 0; i < count; i++) { love::graphics::Buffer *buffer = buffers[i]; @@ -949,18 +823,24 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) buffer->retain(); - if (info->buffers[i] != nullptr) - info->buffers[i]->release(); + int resourceindex = info->resourceIndex + i; - info->buffers[i] = buffer; + if (activeBuffers[resourceindex] != nullptr) + activeBuffers[resourceindex]->release(); - if (texelbinding) + activeBuffers[resourceindex] = buffer; + + if (info->location < 0) + continue; + + int bindingindex = info->location + i; + if (texelbinding && bindingindex >= 0) { - textureBindings[info->ints[i]].texture = getMTLTexture(buffer); + textureBindings[bindingindex].texture = getMTLTexture(buffer); } - else if (storagebinding) + else if (storagebinding && bindingindex >= 0) { - auto &binding = bufferBindings[info->ints[i]]; + auto &binding = bufferBindings[bindingindex]; if (isdefault && (binding.access & ACCESS_WRITE) != 0) binding.buffer = nil; else @@ -987,11 +867,6 @@ static EShLanguage getGLSLangStage(ShaderStageType stage) } } -bool Shader::hasUniform(const std::string &name) const -{ - return uniforms.find(name) != uniforms.end(); -} - id Shader::getCachedRenderPipeline(const RenderPipelineKey &key) { auto it = cachedRenderPipelines.find(key); diff --git a/src/modules/graphics/metal/StreamBuffer.mm b/src/modules/graphics/metal/StreamBuffer.mm index 41d938ca3..00d3e33c9 100644 --- a/src/modules/graphics/metal/StreamBuffer.mm +++ b/src/modules/graphics/metal/StreamBuffer.mm @@ -48,6 +48,8 @@ if (buffer == nil) throw love::Exception("Out of graphics memory."); + buffer.label = [NSString stringWithFormat:@"StreamBuffer (usage: %d, size: %ld)", usage, size]; + data = (uint8 *) buffer.contents; for (int i = 0; i < BUFFER_FRAMES; i++) @@ -65,6 +67,11 @@ } }} + size_t getGPUReadOffset() const override + { + return (frameIndex * bufferSize) + frameGPUReadOffset; + } + MapInfo map(size_t /*minsize*/) override { // Make sure this frame's section of the buffer is done being used. diff --git a/src/modules/graphics/opengl/OpenGL.cpp b/src/modules/graphics/opengl/OpenGL.cpp index 533c2755f..68446af0a 100644 --- a/src/modules/graphics/opengl/OpenGL.cpp +++ b/src/modules/graphics/opengl/OpenGL.cpp @@ -2162,8 +2162,6 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat) if (GLAD_ES_VERSION_3_0 || GLAD_VERSION_3_0 || ((GLAD_ARB_framebuffer_sRGB || GLAD_EXT_framebuffer_sRGB) && (GLAD_VERSION_2_1 || GLAD_EXT_texture_sRGB))) flags |= commonrender; - if (GLAD_VERSION_4_3 || GLAD_ES_VERSION_3_1) - flags |= computewrite; break; case PIXELFORMAT_BGRA8_UNORM: case PIXELFORMAT_BGRA8_sRGB: @@ -2190,7 +2188,7 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat) flags |= commonsample | commonrender; if (GLAD_ES_VERSION_3_0 || (GLAD_OES_texture_half_float && GLAD_EXT_texture_rg)) flags |= commonsample; - if (GLAD_EXT_color_buffer_half_float && (GLAD_ES_VERSION_3_0 || GLAD_EXT_texture_rg)) + if ((GLAD_EXT_color_buffer_half_float || GLAD_EXT_color_buffer_float) && (GLAD_ES_VERSION_3_0 || GLAD_EXT_texture_rg)) flags |= commonrender; if (!(GLAD_VERSION_1_1 || GLAD_ES_VERSION_3_0 || GLAD_OES_texture_half_float_linear)) flags &= ~PIXELFORMATUSAGEFLAGS_LINEAR; @@ -2202,7 +2200,7 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat) flags |= commonsample | commonrender; if (GLAD_ES_VERSION_3_0 || GLAD_OES_texture_half_float) flags |= commonsample; - if (GLAD_EXT_color_buffer_half_float) + if (GLAD_EXT_color_buffer_half_float || GLAD_EXT_color_buffer_float) flags |= commonrender; if (!(GLAD_VERSION_1_1 || GLAD_ES_VERSION_3_0 || GLAD_OES_texture_half_float_linear)) flags &= ~PIXELFORMATUSAGEFLAGS_LINEAR; @@ -2218,6 +2216,8 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat) flags |= commonsample | commonrender; if (GLAD_ES_VERSION_3_0 || (GLAD_OES_texture_float && GLAD_EXT_texture_rg)) flags |= commonsample; + if (GLAD_EXT_color_buffer_float) + flags |= commonrender; if (!(GLAD_VERSION_1_1 || GLAD_ES_VERSION_3_0 || GLAD_OES_texture_half_float_linear)) flags &= ~PIXELFORMATUSAGEFLAGS_LINEAR; if (GLAD_VERSION_4_3) @@ -2228,6 +2228,8 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat) flags |= commonsample | commonrender; if (GLAD_ES_VERSION_3_0 || GLAD_OES_texture_float) flags |= commonsample; + if (GLAD_EXT_color_buffer_float) + flags |= commonrender; if (!(GLAD_VERSION_1_1 || GLAD_OES_texture_float_linear)) flags &= ~PIXELFORMATUSAGEFLAGS_LINEAR; if (GLAD_VERSION_4_3 || GLAD_ES_VERSION_3_1) @@ -2295,10 +2297,12 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat) flags |= computewrite; break; case PIXELFORMAT_RG11B10_FLOAT: - if (GLAD_VERSION_3_0 || GLAD_EXT_packed_float || GLAD_APPLE_texture_packed_float) + if (GLAD_ES_VERSION_3_1 || GLAD_VERSION_3_0 || GLAD_EXT_packed_float || GLAD_APPLE_texture_packed_float) flags |= commonsample; if (GLAD_VERSION_3_0 || GLAD_EXT_packed_float || GLAD_APPLE_color_buffer_packed_float) flags |= commonrender; + if (GLAD_EXT_color_buffer_float) + flags |= commonrender; if (GLAD_VERSION_4_3) flags |= computewrite; break; diff --git a/src/modules/graphics/opengl/Shader.cpp b/src/modules/graphics/opengl/Shader.cpp index d9f7343e8..21e26fb0f 100644 --- a/src/modules/graphics/opengl/Shader.cpp +++ b/src/modules/graphics/opengl/Shader.cpp @@ -38,11 +38,6 @@ namespace graphics namespace opengl { -static bool isBuffer(Shader::UniformType utype) -{ - return utype == Shader::UNIFORM_TEXELBUFFER || utype == Shader::UNIFORM_STORAGEBUFFER; -} - Shader::Shader(StrongRef stages[SHADERSTAGE_MAX_ENUM], const CompileOptions &options) : love::graphics::Shader(stages, options) , program(0) @@ -58,32 +53,11 @@ Shader::~Shader() { unloadVolatile(); - for (const auto &p : uniforms) + for (const auto &p : reflection.allUniforms) { // Allocated with malloc(). - if (p.second.data != nullptr) - free(p.second.data); - - if (p.second.baseType == UNIFORM_SAMPLER || p.second.baseType == UNIFORM_STORAGETEXTURE) - { - for (int i = 0; i < p.second.count; i++) - { - if (p.second.textures[i] != nullptr) - p.second.textures[i]->release(); - } - - delete[] p.second.textures; - } - else if (isBuffer(p.second.baseType)) - { - for (int i = 0; i < p.second.count; i++) - { - if (p.second.buffers[i] != nullptr) - p.second.buffers[i]->release(); - } - - delete[] p.second.buffers; - } + if (p.second->data != nullptr) + free(p.second->data); } } @@ -96,6 +70,26 @@ void Shader::mapActiveUniforms() builtinUniformInfo[i] = nullptr; } + // Make sure all stored resources have their Volatiles loaded before + // the sendTextures/sendBuffers calls below, since they call getHandle(). + for (love::graphics::Texture *tex : activeTextures) + { + if (tex == nullptr) + continue; + Volatile *v = dynamic_cast(tex); + if (v != nullptr) + v->loadVolatile(); + } + + for (love::graphics::Buffer *buffer : activeBuffers) + { + if (buffer == nullptr) + continue; + Volatile *v = dynamic_cast(buffer); + if (v != nullptr) + v->loadVolatile(); + } + GLint activeprogram = 0; glGetIntegerv(GL_CURRENT_PROGRAM, &activeprogram); @@ -107,43 +101,39 @@ void Shader::mapActiveUniforms() GLchar cname[256]; const GLint bufsize = (GLint) (sizeof(cname) / sizeof(GLchar)); - std::map olduniforms = uniforms; - uniforms.clear(); - - auto gfx = Module::getInstance(Module::M_GRAPHICS); - for (int uindex = 0; uindex < numuniforms; uindex++) { GLsizei namelen = 0; GLenum gltype = 0; - UniformInfo u = {}; + int count = 0; - glGetActiveUniform(program, (GLuint) uindex, bufsize, &namelen, &u.count, &gltype, cname); + glGetActiveUniform(program, (GLuint) uindex, bufsize, &namelen, &count, &gltype, cname); - u.name = std::string(cname, (size_t) namelen); - u.location = glGetUniformLocation(program, u.name.c_str()); - u.access = ACCESS_READ; - computeUniformTypeInfo(gltype, u); + std::string name(cname, (size_t) namelen); + int location = glGetUniformLocation(program, name.c_str()); + + if (location == -1) + continue; - // glGetActiveUniform appends "[0]" to the end of array uniform names... - if (u.name.length() > 3) + name = canonicaliizeUniformName(name); + + const auto &uniformit = reflection.allUniforms.find(name); + if (uniformit == reflection.allUniforms.end()) { - size_t findpos = u.name.find("[0]"); - if (findpos != std::string::npos && findpos == u.name.length() - 3) - u.name.erase(u.name.length() - 3); + handleUnknownUniformName(name.c_str()); + continue; } + UniformInfo &u = *uniformit->second; + + u.active = true; + u.location = location; + // If this is a built-in (LOVE-created) uniform, store the location. BuiltinUniform builtin = BUILTIN_MAX_ENUM; if (getConstant(u.name.c_str(), builtin)) builtinUniforms[int(builtin)] = u.location; - if (u.location == -1) - continue; - - if (!fillUniformReflectionData(u)) - continue; - if ((u.baseType == UNIFORM_SAMPLER && builtin != BUILTIN_TEXTURE_MAIN) || u.baseType == UNIFORM_TEXELBUFFER) { TextureUnit unit; @@ -175,183 +165,51 @@ void Shader::mapActiveUniforms() storageTextureBindings.push_back(binding); } - // Make sure previously set uniform data is preserved, and shader- - // initialized values are retrieved. - auto oldu = olduniforms.find(u.name); - if (oldu != olduniforms.end()) + if (u.dataSize == 0) { - u.data = oldu->second.data; - u.dataSize = oldu->second.dataSize; - u.textures = oldu->second.textures; + if (u.baseType == UNIFORM_MATRIX) + u.dataSize = sizeof(uint32) * u.matrix.rows * u.matrix.columns * u.count; + else + u.dataSize = sizeof(uint32) * u.components * u.count; - updateUniform(&u, u.count, true); - } - else - { - u.dataSize = 0; + u.data = malloc(u.dataSize); + memset(u.data, 0, u.dataSize); - switch (u.baseType) + const auto &valuesit = reflection.localUniformInitializerValues.find(u.name); + if (valuesit != reflection.localUniformInitializerValues.end()) { - case UNIFORM_FLOAT: - u.dataSize = sizeof(float) * u.components * u.count; - u.data = malloc(u.dataSize); - break; - case UNIFORM_INT: - case UNIFORM_BOOL: - case UNIFORM_SAMPLER: - case UNIFORM_STORAGETEXTURE: - case UNIFORM_TEXELBUFFER: - u.dataSize = sizeof(int) * u.components * u.count; - u.data = malloc(u.dataSize); - break; - case UNIFORM_UINT: - u.dataSize = sizeof(unsigned int) * u.components * u.count; - u.data = malloc(u.dataSize); - break; - case UNIFORM_MATRIX: - u.dataSize = sizeof(float) * ((size_t)u.matrix.rows * u.matrix.columns) * u.count; - u.data = malloc(u.dataSize); - break; - default: - break; + const auto &values = valuesit->second; + if (!values.empty()) + memcpy(u.data, values.data(), std::min(u.dataSize, sizeof(LocalUniformValue) * values.size())); } + } - if (u.dataSize > 0) - { - memset(u.data, 0, u.dataSize); - - if (u.baseType == UNIFORM_SAMPLER || u.baseType == UNIFORM_TEXELBUFFER) - { - int startunit = (int) textureUnits.size() - u.count; - - if (builtin == BUILTIN_TEXTURE_MAIN) - startunit = 0; - - for (int i = 0; i < u.count; i++) - u.ints[i] = startunit + i; - - glUniform1iv(u.location, u.count, u.ints); - - if (u.baseType == UNIFORM_TEXELBUFFER) - { - u.buffers = new love::graphics::Buffer*[u.count]; - memset(u.buffers, 0, sizeof(Buffer *) * u.count); - } - else - { - u.textures = new love::graphics::Texture*[u.count]; - - auto *tex = gfx->getDefaultTexture(u.textureType, u.dataBaseType); - for (int i = 0; i < u.count; i++) - { - tex->retain(); - u.textures[i] = tex; - } - } - } - else if (u.baseType == UNIFORM_STORAGETEXTURE) - { - int startbinding = (int) storageTextureBindings.size() - u.count; - for (int i = 0; i < u.count; i++) - u.ints[i] = startbinding + i; - - glUniform1iv(u.location, u.count, u.ints); - - u.textures = new love::graphics::Texture*[u.count]; - - if ((u.access & ACCESS_WRITE) != 0) - { - memset(u.textures, 0, sizeof(Texture *) * u.count); - } - else - { - auto *tex = gfx->getDefaultTexture(u.textureType, u.dataBaseType); - for (int i = 0; i < u.count; i++) - { - tex->retain(); - u.textures[i] = tex; - } - } - } - } + if (u.baseType == UNIFORM_SAMPLER || u.baseType == UNIFORM_TEXELBUFFER) + { + int startunit = (int) textureUnits.size() - u.count; - size_t offset = 0; + if (builtin == BUILTIN_TEXTURE_MAIN) + startunit = 0; - // Store any shader-initialized values in our own memory. for (int i = 0; i < u.count; i++) - { - GLint location = u.location; - - if (u.count > 1) - { - std::ostringstream ss; - ss << i; - - std::string indexname = u.name + "[" + ss.str() + "]"; - location = glGetUniformLocation(program, indexname.c_str()); - } - - if (location == -1) - continue; - - switch (u.baseType) - { - case UNIFORM_FLOAT: - glGetUniformfv(program, location, &u.floats[offset]); - offset += u.components; - break; - case UNIFORM_INT: - case UNIFORM_BOOL: - glGetUniformiv(program, location, &u.ints[offset]); - offset += u.components; - break; - case UNIFORM_UINT: - glGetUniformuiv(program, location, &u.uints[offset]); - offset += u.components; - break; - case UNIFORM_MATRIX: - glGetUniformfv(program, location, &u.floats[offset]); - offset += (size_t)u.matrix.rows * u.matrix.columns; - break; - default: - break; - } - } + u.ints[i] = startunit + i; + } + else if (u.baseType == UNIFORM_STORAGETEXTURE) + { + int startbinding = (int) storageTextureBindings.size() - u.count; + for (int i = 0; i < u.count; i++) + u.ints[i] = startbinding + i; } - uniforms[u.name] = u; + updateUniform(&u, u.count, true); if (builtin != BUILTIN_MAX_ENUM) - builtinUniformInfo[(int)builtin] = &uniforms[u.name]; + builtinUniformInfo[(int)builtin] = &u; if (u.baseType == UNIFORM_SAMPLER || u.baseType == UNIFORM_STORAGETEXTURE) - { - // Make sure all stored textures have their Volatiles loaded before - // the sendTextures call, since it calls getHandle(). - for (int i = 0; i < u.count; i++) - { - if (u.textures[i] == nullptr) - continue; - Volatile *v = dynamic_cast(u.textures[i]); - if (v != nullptr) - v->loadVolatile(); - } - - sendTextures(&u, u.textures, u.count, true); - } + sendTextures(&u, &activeTextures[u.resourceIndex], u.count, true); else if (u.baseType == UNIFORM_TEXELBUFFER) - { - for (int i = 0; i < u.count; i++) - { - if (u.buffers[i] == nullptr) - continue; - Volatile *v = dynamic_cast(u.buffers[i]); - if (v != nullptr) - v->loadVolatile(); - } - - sendBuffers(&u, u.buffers, u.count, true); - } + sendBuffers(&u, &activeBuffers[u.resourceIndex], u.count, true); } if (gl.isBufferUsageSupported(BUFFERUSAGE_SHADER_STORAGE)) @@ -364,37 +222,28 @@ void Shader::mapActiveUniforms() for (int sindex = 0; sindex < numstoragebuffers; sindex++) { - UniformInfo u = {}; - u.baseType = UNIFORM_STORAGEBUFFER; - u.access = ACCESS_READ; - GLsizei namelength = 0; glGetProgramResourceName(program, GL_SHADER_STORAGE_BLOCK, sindex, 2048, &namelength, namebuffer); - u.name = std::string(namebuffer, namelength); - u.count = 1; - - if (!fillUniformReflectionData(u)) - continue; + std::string name = canonicaliizeUniformName(std::string(namebuffer, namelength)); - // Make sure previously set uniform data is preserved, and shader- - // initialized values are retrieved. - auto oldu = olduniforms.find(u.name); - if (oldu != olduniforms.end()) + const auto &uniformit = reflection.storageBuffers.find(name); + if (uniformit == reflection.storageBuffers.end()) { - u.data = oldu->second.data; - u.dataSize = oldu->second.dataSize; - u.buffers = oldu->second.buffers; + handleUnknownUniformName(name.c_str()); + continue; } - else - { - u.dataSize = sizeof(int) * 1; - u.data = malloc(u.dataSize); - u.ints[0] = -1; + UniformInfo &u = uniformit->second; + + u.active = true; - u.buffers = new love::graphics::Buffer * [u.count]; - memset(u.buffers, 0, sizeof(Buffer*)* u.count); + if (u.dataSize == 0) + { + u.dataSize = sizeof(int) * u.count; + u.data = malloc(u.dataSize); + for (int i = 0; i < u.count; i++) + u.ints[i] = -1; } // Unlike local uniforms and attributes, OpenGL doesn't auto-assign storage @@ -417,56 +266,13 @@ void Shader::mapActiveUniforms() if (u.access & ACCESS_WRITE) { p.second = (int)activeWritableStorageBuffers.size(); - activeWritableStorageBuffers.push_back(u.buffers[0]); + activeWritableStorageBuffers.push_back(activeBuffers[u.resourceIndex]); } storageBufferBindingIndexToActiveBinding[binding.bindingindex] = p; } - uniforms[u.name] = u; - - for (int i = 0; i < u.count; i++) - { - if (u.buffers[i] == nullptr) - continue; - Volatile* v = dynamic_cast(u.buffers[i]); - if (v != nullptr) - v->loadVolatile(); - } - - sendBuffers(&u, u.buffers, u.count, true); - } - } - - // Make sure uniforms that existed before but don't exist anymore are - // cleaned up. This theoretically shouldn't happen, but... - for (const auto &p : olduniforms) - { - if (uniforms.find(p.first) == uniforms.end()) - { - if (p.second.data != nullptr) - free(p.second.data); - - if (p.second.baseType == UNIFORM_SAMPLER || p.second.baseType == UNIFORM_STORAGETEXTURE) - { - for (int i = 0; i < p.second.count; i++) - { - if (p.second.textures[i] != nullptr) - p.second.textures[i]->release(); - } - - delete[] p.second.textures; - } - else if (isBuffer(p.second.baseType)) - { - for (int i = 0; i < p.second.count; i++) - { - if (p.second.buffers[i] != nullptr) - p.second.buffers[i]->release(); - } - - delete[] p.second.buffers; - } + sendBuffers(&u, &activeBuffers[u.resourceIndex], u.count, true); } } @@ -600,7 +406,7 @@ std::string Shader::getWarnings() const const std::string &stagewarnings = stage->getWarnings(); - if (ShaderStage::getConstant(stage->getStageType(), stagestr)) + if (!stagewarnings.empty() && ShaderStage::getConstant(stage->getStageType(), stagestr)) warnings += std::string(stagestr) + std::string(" shader:\n") + stagewarnings; } @@ -649,16 +455,6 @@ void Shader::attach() } } -const Shader::UniformInfo *Shader::getUniformInfo(const std::string &name) const -{ - const auto it = uniforms.find(name); - - if (it == uniforms.end()) - return nullptr; - - return &(it->second); -} - const Shader::UniformInfo *Shader::getUniformInfo(BuiltinUniform builtin) const { return builtinUniformInfo[(int)builtin]; @@ -802,10 +598,12 @@ void Shader::sendTextures(const UniformInfo *info, love::graphics::Texture **tex tex->retain(); - if (info->textures[i] != nullptr) - info->textures[i]->release(); + int resourceindex = info->resourceIndex + i; - info->textures[i] = tex; + if (activeTextures[resourceindex] != nullptr) + activeTextures[resourceindex]->release(); + + activeTextures[resourceindex] = tex; if (isstoragetex) { @@ -885,10 +683,12 @@ void Shader::sendBuffers(const UniformInfo *info, love::graphics::Buffer **buffe buffer->retain(); - if (info->buffers[i] != nullptr) - info->buffers[i]->release(); + int resourceindex = info->resourceIndex + i; + + if (activeBuffers[resourceindex] != nullptr) + activeBuffers[resourceindex]->release(); - info->buffers[i] = buffer; + activeBuffers[resourceindex] = buffer; if (texelbinding) { @@ -926,11 +726,6 @@ void Shader::flushBatchedDraws() const Graphics::flushBatchedDrawsGlobal(); } -bool Shader::hasUniform(const std::string &name) const -{ - return uniforms.find(name) != uniforms.end(); -} - ptrdiff_t Shader::getHandle() const { return program; @@ -1043,293 +838,6 @@ void Shader::updateBuiltinUniforms(love::graphics::Graphics *gfx, int viewportW, } } -int Shader::getUniformTypeComponents(GLenum type) const -{ - switch (type) - { - case GL_INT: - case GL_UNSIGNED_INT: - case GL_FLOAT: - case GL_BOOL: - return 1; - case GL_INT_VEC2: - case GL_UNSIGNED_INT_VEC2: - case GL_FLOAT_VEC2: - case GL_FLOAT_MAT2: - case GL_BOOL_VEC2: - return 2; - case GL_INT_VEC3: - case GL_UNSIGNED_INT_VEC3: - case GL_FLOAT_VEC3: - case GL_FLOAT_MAT3: - case GL_BOOL_VEC3: - return 3; - case GL_INT_VEC4: - case GL_UNSIGNED_INT_VEC4: - case GL_FLOAT_VEC4: - case GL_FLOAT_MAT4: - case GL_BOOL_VEC4: - return 4; - default: - return 1; - } -} - -Shader::MatrixSize Shader::getMatrixSize(GLenum type) const -{ - MatrixSize m; - - switch (type) - { - case GL_FLOAT_MAT2: - m.columns = m.rows = 2; - break; - case GL_FLOAT_MAT3: - m.columns = m.rows = 3; - break; - case GL_FLOAT_MAT4: - m.columns = m.rows = 4; - break; - case GL_FLOAT_MAT2x3: - m.columns = 2; - m.rows = 3; - break; - case GL_FLOAT_MAT2x4: - m.columns = 2; - m.rows = 4; - break; - case GL_FLOAT_MAT3x2: - m.columns = 3; - m.rows = 2; - break; - case GL_FLOAT_MAT3x4: - m.columns = 3; - m.rows = 4; - break; - case GL_FLOAT_MAT4x2: - m.columns = 4; - m.rows = 2; - break; - case GL_FLOAT_MAT4x3: - m.columns = 4; - m.rows = 3; - break; - default: - m.columns = m.rows = 0; - break; - } - - return m; -} - -void Shader::computeUniformTypeInfo(GLenum type, UniformInfo &u) -{ - u.isDepthSampler = false; - u.components = getUniformTypeComponents(type); - u.baseType = UNIFORM_UNKNOWN; - - switch (type) - { - case GL_INT: - case GL_INT_VEC2: - case GL_INT_VEC3: - case GL_INT_VEC4: - u.baseType = UNIFORM_INT; - u.dataBaseType = DATA_BASETYPE_INT; - break; - case GL_UNSIGNED_INT: - case GL_UNSIGNED_INT_VEC2: - case GL_UNSIGNED_INT_VEC3: - case GL_UNSIGNED_INT_VEC4: - u.baseType = UNIFORM_UINT; - u.dataBaseType = DATA_BASETYPE_UINT; - break; - case GL_FLOAT: - case GL_FLOAT_VEC2: - case GL_FLOAT_VEC3: - case GL_FLOAT_VEC4: - u.baseType = UNIFORM_FLOAT; - u.dataBaseType = DATA_BASETYPE_FLOAT; - break; - case GL_FLOAT_MAT2: - case GL_FLOAT_MAT3: - case GL_FLOAT_MAT4: - case GL_FLOAT_MAT2x3: - case GL_FLOAT_MAT2x4: - case GL_FLOAT_MAT3x2: - case GL_FLOAT_MAT3x4: - case GL_FLOAT_MAT4x2: - case GL_FLOAT_MAT4x3: - u.baseType = UNIFORM_MATRIX; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.matrix = getMatrixSize(type); - break; - case GL_BOOL: - case GL_BOOL_VEC2: - case GL_BOOL_VEC3: - case GL_BOOL_VEC4: - u.baseType = UNIFORM_BOOL; - u.dataBaseType = DATA_BASETYPE_BOOL; - break; - - case GL_SAMPLER_2D: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_2D; - break; - case GL_SAMPLER_2D_SHADOW: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_2D; - u.isDepthSampler = true; - break; - case GL_INT_SAMPLER_2D: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_INT; - u.textureType = TEXTURE_2D; - break; - case GL_UNSIGNED_INT_SAMPLER_2D: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_UINT; - u.textureType = TEXTURE_2D; - break; - case GL_SAMPLER_2D_ARRAY: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_2D_ARRAY; - break; - case GL_SAMPLER_2D_ARRAY_SHADOW: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_2D_ARRAY; - u.isDepthSampler = true; - break; - case GL_INT_SAMPLER_2D_ARRAY: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_INT; - u.textureType = TEXTURE_2D_ARRAY; - break; - case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_UINT; - u.textureType = TEXTURE_2D_ARRAY; - break; - case GL_SAMPLER_3D: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_VOLUME; - break; - case GL_INT_SAMPLER_3D: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_INT; - u.textureType = TEXTURE_VOLUME; - break; - case GL_UNSIGNED_INT_SAMPLER_3D: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_UINT; - u.textureType = TEXTURE_VOLUME; - break; - case GL_SAMPLER_CUBE: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_CUBE; - break; - case GL_SAMPLER_CUBE_SHADOW: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_CUBE; - u.isDepthSampler = true; - break; - case GL_INT_SAMPLER_CUBE: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_INT; - u.textureType = TEXTURE_CUBE; - break; - case GL_UNSIGNED_INT_SAMPLER_CUBE: - u.baseType = UNIFORM_SAMPLER; - u.dataBaseType = DATA_BASETYPE_UINT; - u.textureType = TEXTURE_CUBE; - break; - - case GL_SAMPLER_BUFFER: - u.baseType = UNIFORM_TEXELBUFFER; - u.dataBaseType = DATA_BASETYPE_FLOAT; - break; - case GL_INT_SAMPLER_BUFFER: - u.baseType = UNIFORM_TEXELBUFFER; - u.dataBaseType = DATA_BASETYPE_INT; - break; - case GL_UNSIGNED_INT_SAMPLER_BUFFER: - u.baseType = UNIFORM_TEXELBUFFER; - u.dataBaseType = DATA_BASETYPE_UINT; - break; - - case GL_IMAGE_2D: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_2D; - break; - case GL_INT_IMAGE_2D: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_INT; - u.textureType = TEXTURE_2D; - break; - case GL_UNSIGNED_INT_IMAGE_2D: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_UINT; - u.textureType = TEXTURE_2D; - break; - case GL_IMAGE_2D_ARRAY: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_2D_ARRAY; - break; - case GL_INT_IMAGE_2D_ARRAY: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_INT; - u.textureType = TEXTURE_2D_ARRAY; - break; - case GL_UNSIGNED_INT_IMAGE_2D_ARRAY: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_UINT; - u.textureType = TEXTURE_2D_ARRAY; - break; - case GL_IMAGE_3D: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_VOLUME; - break; - case GL_INT_IMAGE_3D: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_INT; - u.textureType = TEXTURE_VOLUME; - break; - case GL_UNSIGNED_INT_IMAGE_3D: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_UINT; - u.textureType = TEXTURE_VOLUME; - break; - case GL_IMAGE_CUBE: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_FLOAT; - u.textureType = TEXTURE_CUBE; - break; - case GL_INT_IMAGE_CUBE: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_INT; - u.textureType = TEXTURE_CUBE; - break; - case GL_UNSIGNED_INT_IMAGE_CUBE: - u.baseType = UNIFORM_STORAGETEXTURE; - u.dataBaseType = DATA_BASETYPE_UINT; - u.textureType = TEXTURE_CUBE; - break; - - default: - break; - } -} - } // opengl } // graphics } // love diff --git a/src/modules/graphics/opengl/Shader.h b/src/modules/graphics/opengl/Shader.h index fd1e8c162..134ab97fb 100644 --- a/src/modules/graphics/opengl/Shader.h +++ b/src/modules/graphics/opengl/Shader.h @@ -63,12 +63,10 @@ class Shader final : public love::graphics::Shader, public Volatile void attach() override; std::string getWarnings() const override; int getVertexAttributeIndex(const std::string &name) override; - const UniformInfo *getUniformInfo(const std::string &name) const override; const UniformInfo *getUniformInfo(BuiltinUniform builtin) const override; void updateUniform(const UniformInfo *info, int count) override; void sendTextures(const UniformInfo *info, love::graphics::Texture **textures, int count) override; void sendBuffers(const UniformInfo *info, love::graphics::Buffer **buffers, int count) override; - bool hasUniform(const std::string &name) const override; ptrdiff_t getHandle() const override; void setVideoTextures(love::graphics::Texture *ytexture, love::graphics::Texture *cbtexture, love::graphics::Texture *crtexture) override; @@ -100,10 +98,6 @@ class Shader final : public love::graphics::Shader, public Volatile void sendTextures(const UniformInfo *info, love::graphics::Texture **textures, int count, bool internalupdate); void sendBuffers(const UniformInfo *info, love::graphics::Buffer **buffers, int count, bool internalupdate); - int getUniformTypeComponents(GLenum type) const; - void computeUniformTypeInfo(GLenum type, UniformInfo &u); - MatrixSize getMatrixSize(GLenum type) const; - void flushBatchedDraws() const; // Get any warnings or errors generated only by the shader program object. @@ -120,9 +114,6 @@ class Shader final : public love::graphics::Shader, public Volatile std::map attributes; - // Uniform location buffer map - std::map uniforms; - // Texture unit pool for setting textures std::vector textureUnits; diff --git a/src/modules/graphics/opengl/StreamBuffer.cpp b/src/modules/graphics/opengl/StreamBuffer.cpp index c5919cdd7..9317ead3a 100644 --- a/src/modules/graphics/opengl/StreamBuffer.cpp +++ b/src/modules/graphics/opengl/StreamBuffer.cpp @@ -63,6 +63,11 @@ class StreamBufferClientMemory final : public love::graphics::StreamBuffer delete[] data; } + size_t getGPUReadOffset() const override + { + return (size_t) data; + } + MapInfo map(size_t /*minsize*/) override { return MapInfo(data, bufferSize); @@ -111,6 +116,11 @@ class StreamBufferSubDataOrphan final : public love::graphics::StreamBuffer, pub delete[] data; } + size_t getGPUReadOffset() const override + { + return frameGPUReadOffset; + } + MapInfo map(size_t /*minsize*/) override { if (orphan) @@ -192,6 +202,11 @@ class StreamBufferSync : public love::graphics::StreamBuffer virtual ~StreamBufferSync() {} + size_t getGPUReadOffset() const override + { + return (frameIndex * bufferSize) + frameGPUReadOffset; + } + void nextFrame() override { // Insert a GPU fence for this frame's section of the data, we'll wait diff --git a/src/modules/graphics/vulkan/Graphics.cpp b/src/modules/graphics/vulkan/Graphics.cpp index 9b1ce0f39..7687d8bb6 100644 --- a/src/modules/graphics/vulkan/Graphics.cpp +++ b/src/modules/graphics/vulkan/Graphics.cpp @@ -2354,28 +2354,39 @@ void Graphics::prepareDraw(const VertexAttributes &attributes, const BufferBindi configuration.dynamicState.cullmode = cullmode; } - std::vector bufferVector; - std::vector offsets; + VkBuffer vkbuffers[VertexAttributes::MAX + 2]; + VkDeviceSize vkoffsets[VertexAttributes::MAX + 2]; + int buffercount = 0; - for (uint32_t i = 0; i < VertexAttributes::MAX; i++) + uint32 allbits = buffers.useBits; + uint32 i = 0; + while (allbits) { - if (buffers.useBits & (1u << i)) + uint32 bit = 1u << i; + + if (buffers.useBits & bit) { - bufferVector.push_back((VkBuffer)buffers.info[i].buffer->getHandle()); - offsets.push_back((VkDeviceSize)buffers.info[i].offset); + vkbuffers[buffercount] = (VkBuffer)buffers.info[i].buffer->getHandle(); + vkoffsets[buffercount] = (VkDeviceSize)buffers.info[i].offset; + buffercount++; } + + i++; + allbits >>= 1; } if (!(attributes.enableBits & (1u << ATTRIB_TEXCOORD))) { - bufferVector.push_back((VkBuffer)defaultConstantTexCoord->getHandle()); - offsets.push_back((VkDeviceSize)0); + vkbuffers[buffercount] = (VkBuffer)defaultConstantTexCoord->getHandle(); + vkoffsets[buffercount] = (VkDeviceSize)0; + buffercount++; } if (!(attributes.enableBits & (1u << ATTRIB_COLOR))) { - bufferVector.push_back((VkBuffer)defaultConstantColor->getHandle()); - offsets.push_back((VkDeviceSize)0); + vkbuffers[buffercount] = (VkBuffer)defaultConstantColor->getHandle(); + vkoffsets[buffercount] = (VkDeviceSize)0; + buffercount++; } configuration.shader->setMainTex(texture); @@ -2383,7 +2394,9 @@ void Graphics::prepareDraw(const VertexAttributes &attributes, const BufferBindi ensureGraphicsPipelineConfiguration(configuration); configuration.shader->cmdPushDescriptorSets(commandBuffers.at(currentFrame), VK_PIPELINE_BIND_POINT_GRAPHICS); - vkCmdBindVertexBuffers(commandBuffers.at(currentFrame), 0, static_cast(bufferVector.size()), bufferVector.data(), offsets.data()); + + if (buffercount > 0) + vkCmdBindVertexBuffers(commandBuffers.at(currentFrame), 0, static_cast(buffercount), vkbuffers, vkoffsets); } void Graphics::setDefaultRenderPass() diff --git a/src/modules/graphics/vulkan/Graphics.h b/src/modules/graphics/vulkan/Graphics.h index 084c2b836..d64aa3bff 100644 --- a/src/modules/graphics/vulkan/Graphics.h +++ b/src/modules/graphics/vulkan/Graphics.h @@ -449,7 +449,7 @@ class Graphics final : public love::graphics::Graphics std::vector>> cleanUpFunctions; std::vector>> readbackCallbacks; std::vector screenshotReadbackBuffers; - std::set usedShadersInFrame; + std::set> usedShadersInFrame; RenderpassState renderPassState; }; diff --git a/src/modules/graphics/vulkan/Shader.cpp b/src/modules/graphics/vulkan/Shader.cpp index 63e90b601..f28b8bfd8 100644 --- a/src/modules/graphics/vulkan/Shader.cpp +++ b/src/modules/graphics/vulkan/Shader.cpp @@ -21,6 +21,7 @@ #include "graphics/vertex.h" #include "Shader.h" #include "Graphics.h" +#include "common/Range.h" #include "libraries/glslang/glslang/Public/ShaderLang.h" #include "libraries/glslang/glslang/Public/ResourceLimits.h" @@ -41,59 +42,60 @@ static const uint32_t DESCRIPTOR_POOL_SIZE = 1000; class BindingMapper { public: - uint32_t operator()(spirv_cross::CompilerGLSL &comp, std::vector &spirv, const std::string &name, const spirv_cross::ID &id) + uint32_t operator()(spirv_cross::CompilerGLSL &comp, std::vector &spirv, const std::string &name, int count, const spirv_cross::ID &id) { auto it = bindingMappings.find(name); if (it == bindingMappings.end()) { auto binding = comp.get_decoration(id, spv::DecorationBinding); - if (isFreeBinding(binding)) + if (isFreeBinding(binding, count)) { - bindingMappings[name] = binding; + bindingMappings[name] = Range(binding, count); return binding; } else { - uint32_t freeBinding = getFreeBinding(); + uint32_t freeBinding = getFreeBinding(count); uint32_t binaryBindingOffset; if (!comp.get_binary_offset_for_decoration(id, spv::DecorationBinding, binaryBindingOffset)) - throw love::Exception("could not get binary offset for binding"); + throw love::Exception("could not get binary offset for uniform %s binding", name.c_str()); spirv[binaryBindingOffset] = freeBinding; - bindingMappings[name] = freeBinding; + bindingMappings[name] = Range(freeBinding, count); return freeBinding; } } else - return it->second; + return (uint32_t)it->second.getOffset(); }; private: - uint32_t getFreeBinding() + uint32_t getFreeBinding(int count) { for (uint32_t i = 0;; i++) { - if (isFreeBinding(i)) + if (isFreeBinding(i, count)) return i; } } - bool isFreeBinding(uint32_t binding) + bool isFreeBinding(uint32_t binding, int count) { + Range r(binding, count); for (const auto &entry : bindingMappings) { - if (entry.second == binding) + if (entry.second.intersects(r)) return false; } return true; } - std::map bindingMappings; + std::map bindingMappings; }; @@ -173,31 +175,6 @@ void Shader::unloadVolatile() if (shaderModules.empty()) return; - for (const auto &uniform : uniformInfos) - { - switch (uniform.second.baseType) - { - case UNIFORM_SAMPLER: - case UNIFORM_STORAGETEXTURE: - for (int i = 0; i < uniform.second.count; i++) - { - if (uniform.second.textures[i] != nullptr) - uniform.second.textures[i]->release(); - } - delete[] uniform.second.textures; - break; - case UNIFORM_TEXELBUFFER: - case UNIFORM_STORAGEBUFFER: - for (int i = 0; i < uniform.second.count; i++) - { - if (uniform.second.buffers[i] != nullptr) - uniform.second.buffers[i]->release(); - } - delete[] uniform.second.buffers; - break; - } - } - vgfx->queueCleanUp([shaderModules = std::move(shaderModules), device = device, descriptorSetLayout = descriptorSetLayout, pipelineLayout = pipelineLayout, descriptorPools = descriptorPools, computePipeline = computePipeline](){ for (const auto &pools : descriptorPools) { @@ -254,7 +231,7 @@ void Shader::newFrame() streamBuffers.clear(); streamBuffers.push_back(new StreamBuffer(vgfx, BUFFERUSAGE_UNIFORM, newSize)); } - else + else if (streamBuffers.size() == 1) streamBuffers.at(0)->nextFrame(); for (VkDescriptorPool pool : descriptorPools[currentFrame]) @@ -263,18 +240,9 @@ void Shader::newFrame() void Shader::cmdPushDescriptorSets(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint) { - VkDescriptorSet currentDescriptorSet = allocateDescriptorSet(); - - std::vector bufferInfos; - bufferInfos.reserve(numBuffers); - - std::vector imageInfos; - imageInfos.reserve(numTextures); - - std::vector bufferViews; - bufferViews.reserve(numBufferViews); - - std::vector descriptorWrites; + int imageIndex = 0; + int bufferIndex = 0; + int bufferViewIndex = 0; if (!localUniformData.empty()) { @@ -299,120 +267,98 @@ void Shader::cmdPushDescriptorSets(VkCommandBuffer commandBuffer, VkPipelineBind auto offset = currentStreamBuffer->unmap(uniformBufferSizeAligned); currentStreamBuffer->markUsed(uniformBufferSizeAligned); - VkDescriptorBufferInfo bufferInfo{}; + VkDescriptorBufferInfo &bufferInfo = descriptorBuffers[bufferIndex++]; bufferInfo.buffer = (VkBuffer)currentStreamBuffer->getHandle(); bufferInfo.offset = offset; bufferInfo.range = localUniformData.size(); - bufferInfos.push_back(bufferInfo); - - VkWriteDescriptorSet uniformWrite{}; - uniformWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - uniformWrite.dstSet = currentDescriptorSet; - uniformWrite.dstBinding = localUniformLocation; - uniformWrite.dstArrayElement = 0; - uniformWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - uniformWrite.descriptorCount = 1; - uniformWrite.pBufferInfo = &bufferInfos[bufferInfos.size() - 1]; - - descriptorWrites.push_back(uniformWrite); - currentUsedUniformStreamBuffersCount++; } - for (const auto &u : uniformInfos) + // TODO: iteration order must match the order at the end of compileShaders right now. + // TODO: We can store data via setTextures and setBuffers instead of iterating over + // everything here. + for (const auto &u : reflection.sampledTextures) { - auto &info = u.second; - - if (usesLocalUniformData(&info)) + const auto &info = u.second; + if (!info.active) continue; - if (info.baseType == UNIFORM_SAMPLER || info.baseType == UNIFORM_STORAGETEXTURE) + for (int i = 0; i < info.count; i++) { - bool isSampler = info.baseType == UNIFORM_SAMPLER; - - for (int i = 0; i < info.count; i++) - { - auto vkTexture = dynamic_cast(info.textures[i]); + auto vkTexture = dynamic_cast(activeTextures[info.resourceIndex + i]); - if (vkTexture == nullptr) - throw love::Exception("uniform variable %s is not set.", info.name.c_str()); + if (vkTexture == nullptr) + throw love::Exception("uniform variable %s is not set.", info.name.c_str()); - VkDescriptorImageInfo imageInfo{}; + VkDescriptorImageInfo &imageInfo = descriptorImages[imageIndex++]; - imageInfo.imageLayout = vkTexture->getImageLayout(); - imageInfo.imageView = (VkImageView)vkTexture->getRenderTargetHandle(); - if (isSampler) - imageInfo.sampler = (VkSampler)vkTexture->getSamplerHandle(); - - imageInfos.push_back(imageInfo); - } + imageInfo.imageLayout = vkTexture->getImageLayout(); + imageInfo.imageView = (VkImageView)vkTexture->getRenderTargetHandle(); + imageInfo.sampler = (VkSampler)vkTexture->getSamplerHandle(); + } + } - VkWriteDescriptorSet write{}; - write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = currentDescriptorSet; - write.dstBinding = info.location; - write.dstArrayElement = 0; - if (isSampler) - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - else - write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - write.descriptorCount = static_cast(info.count); - write.pImageInfo = &imageInfos[imageInfos.size() - info.count]; + for (const auto &u : reflection.storageTextures) + { + const auto &info = u.second; + if (!info.active) + continue; - descriptorWrites.push_back(write); - } - if (info.baseType == UNIFORM_STORAGEBUFFER) + for (int i = 0; i < info.count; i++) { - VkWriteDescriptorSet write{}; - write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = currentDescriptorSet; - write.dstBinding = info.location; - write.dstArrayElement = 0; - write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - write.descriptorCount = info.count; - - for (int i = 0; i < info.count; i++) - { - if (info.buffers[i] == nullptr) - throw love::Exception("uniform variable %s is not set.", info.name.c_str()); + auto vkTexture = dynamic_cast(activeTextures[info.resourceIndex + i]); - VkDescriptorBufferInfo bufferInfo{}; - bufferInfo.buffer = (VkBuffer)info.buffers[i]->getHandle();; - bufferInfo.offset = 0; - bufferInfo.range = info.buffers[i]->getSize(); - - bufferInfos.push_back(bufferInfo); - } + if (vkTexture == nullptr) + throw love::Exception("uniform variable %s is not set.", info.name.c_str()); - write.pBufferInfo = &bufferInfos[bufferInfos.size() - info.count]; + VkDescriptorImageInfo &imageInfo = descriptorImages[imageIndex++]; - descriptorWrites.push_back(write); + imageInfo.imageLayout = vkTexture->getImageLayout(); + imageInfo.imageView = (VkImageView)vkTexture->getRenderTargetHandle(); } - if (info.baseType == UNIFORM_TEXELBUFFER) + } + + for (const auto &u : reflection.texelBuffers) + { + const auto &info = u.second; + if (!info.active) + continue; + + for (int i = 0; i < info.count; i++) { - VkWriteDescriptorSet write{}; - write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = currentDescriptorSet; - write.dstBinding = info.location; - write.dstArrayElement = 0; - write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER; - write.descriptorCount = info.count; - - for (int i = 0; i < info.count; i++) - { - if (info.buffers[i] == nullptr) - throw love::Exception("uniform variable %s is not set.", info.name.c_str()); + auto b = activeBuffers[info.resourceIndex + i]; + if (b == nullptr) + throw love::Exception("uniform variable %s is not set.", info.name.c_str()); - bufferViews.push_back((VkBufferView)info.buffers[i]->getTexelBufferHandle()); - } + descriptorBufferViews[bufferViewIndex++] = (VkBufferView)b->getTexelBufferHandle(); + } + } - write.pTexelBufferView = &bufferViews[bufferViews.size() - info.count]; + for (const auto &u : reflection.storageBuffers) + { + const auto &info = u.second; + if (!info.active) + continue; - descriptorWrites.push_back(write); + for (int i = 0; i < info.count; i++) + { + auto b = activeBuffers[info.resourceIndex + i]; + if (b == nullptr) + throw love::Exception("uniform variable %s is not set.", info.name.c_str()); + + VkDescriptorBufferInfo &bufferInfo = descriptorBuffers[bufferIndex++]; + bufferInfo.buffer = (VkBuffer)b->getHandle(); + bufferInfo.offset = 0; + bufferInfo.range = b->getSize(); } } + VkDescriptorSet currentDescriptorSet = allocateDescriptorSet(); + + for (auto &write : descriptorWrites) + write.dstSet = currentDescriptorSet; + vkUpdateDescriptorSets(device, descriptorWrites.size(), descriptorWrites.data(), 0, nullptr); vkCmdBindDescriptorSets(commandBuffer, bindPoint, pipelineLayout, 0, 1, ¤tDescriptorSet, 0, nullptr); @@ -444,12 +390,6 @@ int Shader::getVertexAttributeIndex(const std::string &name) return it == attributes.end() ? -1 : it->second; } -const Shader::UniformInfo *Shader::getUniformInfo(const std::string &name) const -{ - const auto it = uniformInfos.find(name); - return it != uniformInfos.end() ? &(it->second) : nullptr; -} - const Shader::UniformInfo *Shader::getUniformInfo(BuiltinUniform builtin) const { return builtinUniformInfo[builtin]; @@ -466,11 +406,15 @@ void Shader::updateUniform(const UniformInfo *info, int count) void Shader::sendTextures(const UniformInfo *info, graphics::Texture **textures, int count) { + if (current == this) + Graphics::flushBatchedDrawsGlobal(); + for (int i = 0; i < count; i++) { - auto oldTexture = info->textures[i]; - info->textures[i] = textures[i]; - info->textures[i]->retain(); + int resourceindex = info->resourceIndex + i; + auto oldTexture = activeTextures[resourceindex]; + activeTextures[resourceindex] = textures[i]; + activeTextures[resourceindex]->retain(); if (oldTexture) oldTexture->release(); } @@ -478,11 +422,15 @@ void Shader::sendTextures(const UniformInfo *info, graphics::Texture **textures, void Shader::sendBuffers(const UniformInfo *info, love::graphics::Buffer **buffers, int count) { + if (current == this) + Graphics::flushBatchedDrawsGlobal(); + for (int i = 0; i < count; i++) { - auto oldBuffer = info->buffers[i]; - info->buffers[i] = buffers[i]; - info->buffers[i]->retain(); + int resourceindex = info->resourceIndex + i; + auto oldBuffer = activeBuffers[resourceindex]; + activeBuffers[resourceindex] = buffers[i]; + activeBuffers[resourceindex]->retain(); if (oldBuffer) oldBuffer->release(); } @@ -524,38 +472,25 @@ void Shader::buildLocalUniforms(spirv_cross::Compiler &comp, const spirv_cross:: continue; } - UniformInfo u{}; - u.name = name; - u.dataSize = memberSize; - u.count = memberType.array.empty() ? 1 : memberType.array[0]; - u.components = 1; - u.data = localUniformStagingData.data() + offset; + name = canonicaliizeUniformName(name); - if (memberType.columns == 1) + auto uniformit = reflection.allUniforms.find(name); + if (uniformit == reflection.allUniforms.end()) { - if (memberType.basetype == SPIRType::Int) - u.baseType = UNIFORM_INT; - else if (memberType.basetype == SPIRType::UInt) - u.baseType = UNIFORM_UINT; - else - u.baseType = UNIFORM_FLOAT; - u.components = memberType.vecsize; - } - else - { - u.baseType = UNIFORM_MATRIX; - u.matrix.rows = memberType.vecsize; - u.matrix.columns = memberType.columns; + handleUnknownUniformName(name.c_str()); + continue; } - const auto &reflectionIt = validationReflection.localUniforms.find(u.name); - if (reflectionIt != validationReflection.localUniforms.end()) - { - const auto &localUniform = reflectionIt->second; - if (localUniform.dataType == DATA_BASETYPE_BOOL) - u.baseType = UNIFORM_BOOL; + UniformInfo &u = *(uniformit->second); - const auto &values = localUniform.initializerValues; + u.active = true; + u.dataSize = memberSize; + u.data = localUniformStagingData.data() + offset; + + const auto &valuesit = reflection.localUniformInitializerValues.find(name); + if (valuesit != reflection.localUniformInitializerValues.end()) + { + const auto &values = valuesit->second; if (!values.empty()) memcpy( u.data, @@ -563,14 +498,12 @@ void Shader::buildLocalUniforms(spirv_cross::Compiler &comp, const spirv_cross:: std::min(u.dataSize, values.size() * sizeof(LocalUniformValue))); } - uniformInfos[u.name] = u; - BuiltinUniform builtin = BUILTIN_MAX_ENUM; if (getConstant(u.name.c_str(), builtin)) { if (builtin == BUILTIN_UNIFORMS_PER_DRAW) builtinUniformDataOffset = offset; - builtinUniformInfo[builtin] = &uniformInfos[u.name]; + builtinUniformInfo[builtin] = &u; } } } @@ -643,8 +576,6 @@ void Shader::compileShaders() if (!program->mapIO()) throw love::Exception("mapIO failed"); - uniformInfos.clear(); - BindingMapper bindingMapper; for (int i = 0; i < SHADERSTAGE_MAX_ENUM; i++) @@ -679,7 +610,7 @@ void Shader::compileShaders() localUniformStagingData.resize(defaultUniformBlockSize); localUniformData.resize(defaultUniformBlockSize); - localUniformLocation = bindingMapper(comp, spirv, resource.name, resource.id); + localUniformLocation = bindingMapper(comp, spirv, resource.name, 1, resource.id); memset(localUniformStagingData.data(), 0, defaultUniformBlockSize); memset(localUniformData.data(), 0, defaultUniformBlockSize); @@ -695,119 +626,51 @@ void Shader::compileShaders() for (const auto &r : shaderResources.sampled_images) { - const SPIRType &basetype = comp.get_type(r.base_type_id); - const SPIRType &type = comp.get_type(r.type_id); - const SPIRType &imagetype = comp.get_type(basetype.image.type); - - graphics::Shader::UniformInfo info; - info.location = bindingMapper(comp, spirv, r.name, r.id); - info.baseType = UNIFORM_SAMPLER; - info.name = r.name; - info.count = type.array.empty() ? 1 : type.array[0]; - info.isDepthSampler = type.image.depth; - info.components = 1; - - switch (imagetype.basetype) + std::string name = canonicaliizeUniformName(r.name); + auto uniformit = reflection.allUniforms.find(name); + if (uniformit == reflection.allUniforms.end()) { - case SPIRType::Float: - info.dataBaseType = DATA_BASETYPE_FLOAT; - break; - case SPIRType::Int: - info.dataBaseType = DATA_BASETYPE_INT; - break; - case SPIRType::UInt: - info.dataBaseType = DATA_BASETYPE_UINT; - break; - default: - break; - } - - switch (basetype.image.dim) - { - case spv::Dim2D: - info.textureType = basetype.image.arrayed ? TEXTURE_2D_ARRAY : TEXTURE_2D; - info.textures = new love::graphics::Texture *[info.count]; - break; - case spv::Dim3D: - info.textureType = TEXTURE_VOLUME; - info.textures = new love::graphics::Texture *[info.count]; - break; - case spv::DimCube: - if (basetype.image.arrayed) { - throw love::Exception("cubemap arrays are not currently supported"); - } - info.textureType = TEXTURE_CUBE; - info.textures = new love::graphics::Texture *[info.count]; - break; - case spv::DimBuffer: - info.baseType = UNIFORM_TEXELBUFFER; - info.buffers = new love::graphics::Buffer *[info.count]; - break; - default: - throw love::Exception("unknown dim"); + handleUnknownUniformName(name.c_str()); + continue; } - if (info.baseType == UNIFORM_TEXELBUFFER) - { - for (int i = 0; i < info.count; i++) - info.buffers[i] = nullptr; - } - else - { - for (int i = 0; i < info.count; i++) - { - info.textures[i] = nullptr; - } - } + UniformInfo &u = *(uniformit->second); + u.active = true; + u.location = bindingMapper(comp, spirv, name, u.count, r.id); - uniformInfos[r.name] = info; BuiltinUniform builtin; - if (getConstant(r.name.c_str(), builtin)) - builtinUniformInfo[builtin] = &uniformInfos[info.name]; + if (getConstant(name.c_str(), builtin)) + builtinUniformInfo[builtin] = &u; } for (const auto &r : shaderResources.storage_buffers) { - const auto &type = comp.get_type(r.type_id); - - UniformInfo u{}; - u.baseType = UNIFORM_STORAGEBUFFER; - u.components = 1; - u.name = r.name; - u.count = type.array.empty() ? 1 : type.array[0]; - - if (!fillUniformReflectionData(u)) + std::string name = canonicaliizeUniformName(r.name); + const auto &uniformit = reflection.storageBuffers.find(name); + if (uniformit == reflection.storageBuffers.end()) + { + handleUnknownUniformName(name.c_str()); continue; + } - u.location = bindingMapper(comp, spirv, r.name, r.id); - u.buffers = new love::graphics::Buffer *[u.count]; - - for (int i = 0; i < u.count; i++) - u.buffers[i] = nullptr; - - uniformInfos[u.name] = u; + UniformInfo &u = uniformit->second; + u.active = true; + u.location = bindingMapper(comp, spirv, name, u.count, r.id); } for (const auto &r : shaderResources.storage_images) { - const auto &type = comp.get_type(r.type_id); - - UniformInfo u{}; - u.baseType = UNIFORM_STORAGETEXTURE; - u.components = 1; - u.name = r.name; - u.count = type.array.empty() ? 1 : type.array[0]; - - if (!fillUniformReflectionData(u)) + std::string name = canonicaliizeUniformName(r.name); + const auto &uniformit = reflection.storageTextures.find(name); + if (uniformit == reflection.storageTextures.end()) + { + handleUnknownUniformName(name.c_str()); continue; + } - u.textures = new love::graphics::Texture *[u.count]; - u.location = bindingMapper(comp, spirv, r.name, r.id); - - for (int i = 0; i < u.count; i++) - u.textures[i] = nullptr; - - uniformInfos[u.name] = u; + UniformInfo &u = uniformit->second; + u.active = true; + u.location = bindingMapper(comp, spirv, name, u.count, r.id); } if (shaderStage == SHADERSTAGE_VERTEX) @@ -826,7 +689,7 @@ void Shader::compileShaders() uint32_t locationOffset; if (!comp.get_binary_offset_for_decoration(r.id, spv::DecorationLocation, locationOffset)) - throw love::Exception("could not get binary offset for location"); + throw love::Exception("could not get binary offset for vertex attribute %s location", r.name.c_str()); spirv[locationOffset] = (uint32_t)index; @@ -868,31 +731,151 @@ void Shader::compileShaders() shaderStages.push_back(shaderStageInfo); } - numBuffers = 0; - numTextures = 0; - numBufferViews = 0; + int numBuffers = 0; + int numTextures = 0; + int numBufferViews = 0; if (localUniformData.size() > 0) numBuffers++; - for (const auto &u : uniformInfos) + for (const auto kvp : reflection.allUniforms) { - switch (u.second.baseType) + if (!kvp.second->active) + continue; + + switch (kvp.second->baseType) { case UNIFORM_SAMPLER: case UNIFORM_STORAGETEXTURE: - numTextures++; + numTextures += kvp.second->count; break; case UNIFORM_STORAGEBUFFER: - numBuffers++; + numBuffers += kvp.second->count; break; case UNIFORM_TEXELBUFFER: - numBufferViews++; + numBufferViews += kvp.second->count; break; default: continue; } } + + descriptorWrites.clear(); + + descriptorBuffers.clear(); + descriptorBuffers.reserve(numBuffers); + + descriptorImages.clear(); + descriptorImages.reserve(numTextures); + + descriptorBufferViews.clear(); + descriptorBufferViews.reserve(numBufferViews); + + if (localUniformData.size() > 0) + { + VkDescriptorBufferInfo bufferInfo{}; + bufferInfo.range = localUniformData.size(); + + descriptorBuffers.push_back(bufferInfo); + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstBinding = localUniformLocation; + write.dstArrayElement = 0; + write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + write.descriptorCount = 1; + write.pBufferInfo = &descriptorBuffers.back(); + descriptorWrites.push_back(write); + } + + for (const auto &u : reflection.sampledTextures) + { + const UniformInfo &info = u.second; + if (!info.active) + continue; + + for (int i = 0; i < info.count; i++) + { + VkDescriptorImageInfo imageInfo{}; + descriptorImages.push_back(imageInfo); + } + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstBinding = info.location; + write.dstArrayElement = 0; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.descriptorCount = static_cast(info.count); + write.pImageInfo = &descriptorImages[descriptorImages.size() - info.count]; + + descriptorWrites.push_back(write); + } + + for (const auto &u : reflection.storageTextures) + { + const UniformInfo &info = u.second; + if (!info.active) + continue; + + for (int i = 0; i < info.count; i++) + { + VkDescriptorImageInfo imageInfo{}; + descriptorImages.push_back(imageInfo); + } + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstBinding = info.location; + write.dstArrayElement = 0; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + write.descriptorCount = static_cast(info.count); + write.pImageInfo = &descriptorImages[descriptorImages.size() - info.count]; + + descriptorWrites.push_back(write); + } + + for (const auto &u : reflection.texelBuffers) + { + const UniformInfo &info = u.second; + if (!info.active) + continue; + + for (int i = 0; i < info.count; i++) + descriptorBufferViews.push_back(VK_NULL_HANDLE); + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstBinding = info.location; + write.dstArrayElement = 0; + write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; + write.descriptorCount = info.count; + write.pTexelBufferView = &descriptorBufferViews[descriptorBufferViews.size() - info.count]; + + descriptorWrites.push_back(write); + } + + for (const auto &u : reflection.storageBuffers) + { + const UniformInfo &info = u.second; + if (!info.active) + continue; + + for (int i = 0; i < info.count; i++) + { + VkDescriptorBufferInfo bufferInfo{}; + descriptorBuffers.push_back(bufferInfo); + } + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstBinding = info.location; + write.dstArrayElement = 0; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.descriptorCount = info.count; + write.pBufferInfo = &descriptorBuffers[descriptorBuffers.size() - info.count]; + + descriptorWrites.push_back(write); + } } void Shader::createDescriptorSetLayout() @@ -905,16 +888,19 @@ void Shader::createDescriptorSetLayout() else stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; - for (auto const &entry : uniformInfos) + for (auto const &entry : reflection.allUniforms) { - auto type = Vulkan::getDescriptorType(entry.second.baseType); + if (!entry.second->active) + continue; + + auto type = Vulkan::getDescriptorType(entry.second->baseType); if (type != VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER) { VkDescriptorSetLayoutBinding layoutBinding{}; - layoutBinding.binding = entry.second.location; + layoutBinding.binding = entry.second->location; layoutBinding.descriptorType = type; - layoutBinding.descriptorCount = entry.second.count; + layoutBinding.descriptorCount = entry.second->count; layoutBinding.stageFlags = stageFlags; bindings.push_back(layoutBinding); @@ -976,10 +962,13 @@ void Shader::createDescriptorPoolSizes() descriptorPoolSizes.push_back(size); } - for (const auto &entry : uniformInfos) + for (const auto &entry : reflection.allUniforms) { + if (entry.second->location < 0) + continue; + VkDescriptorPoolSize size{}; - auto type = Vulkan::getDescriptorType(entry.second.baseType); + auto type = Vulkan::getDescriptorType(entry.second->baseType); if (type == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER) continue; @@ -1012,29 +1001,26 @@ void Shader::setVideoTextures(graphics::Texture *ytexture, graphics::Texture *cb for (size_t i = 0; i < textures.size(); i++) { - if (builtinUniformInfo[builtIns[i]] != nullptr) + const UniformInfo *u = builtinUniformInfo[builtIns[i]]; + if (u != nullptr) { textures[i]->retain(); - if (builtinUniformInfo[builtIns[i]]->textures[0]) - builtinUniformInfo[builtIns[i]]->textures[0]->release(); - builtinUniformInfo[builtIns[i]]->textures[0] = textures[i]; + if (activeTextures[u->resourceIndex]) + activeTextures[u->resourceIndex]->release(); + activeTextures[u->resourceIndex] = textures[i]; } } } -bool Shader::hasUniform(const std::string &name) const -{ - return uniformInfos.find(name) != uniformInfos.end(); -} - void Shader::setMainTex(graphics::Texture *texture) { - if (builtinUniformInfo[BUILTIN_TEXTURE_MAIN] != nullptr) + const UniformInfo *u = builtinUniformInfo[BUILTIN_TEXTURE_MAIN]; + if (u != nullptr) { texture->retain(); - if (builtinUniformInfo[BUILTIN_TEXTURE_MAIN]->textures[0]) - builtinUniformInfo[BUILTIN_TEXTURE_MAIN]->textures[0]->release(); - builtinUniformInfo[BUILTIN_TEXTURE_MAIN]->textures[0] = texture; + if (activeTextures[u->resourceIndex]) + activeTextures[u->resourceIndex]->release(); + activeTextures[u->resourceIndex] = texture; } } diff --git a/src/modules/graphics/vulkan/Shader.h b/src/modules/graphics/vulkan/Shader.h index 58632285c..7639fdaf4 100644 --- a/src/modules/graphics/vulkan/Shader.h +++ b/src/modules/graphics/vulkan/Shader.h @@ -76,7 +76,6 @@ class Shader final int getVertexAttributeIndex(const std::string &name) override; - const UniformInfo *getUniformInfo(const std::string &name) const override; const UniformInfo *getUniformInfo(BuiltinUniform builtin) const override; void updateUniform(const UniformInfo *info, int count) override; @@ -84,8 +83,6 @@ class Shader final void sendTextures(const UniformInfo *info, graphics::Texture **textures, int count) override; void sendBuffers(const UniformInfo *info, love::graphics::Buffer **buffers, int count) override; - bool hasUniform(const std::string &name) const override; - void setVideoTextures(graphics::Texture *ytexture, graphics::Texture *cbtexture, graphics::Texture *crtexture) override; void setMainTex(graphics::Texture *texture); @@ -105,10 +102,6 @@ class Shader final VkPipeline computePipeline; - uint32_t numTextures; - uint32_t numBuffers; - uint32_t numBufferViews; - VkDescriptorSetLayout descriptorSetLayout; VkPipelineLayout pipelineLayout; std::vector descriptorPoolSizes; @@ -118,6 +111,11 @@ class Shader final std::vector streamBuffers; std::vector> descriptorPools; + std::vector descriptorBuffers; + std::vector descriptorImages; + std::vector descriptorBufferViews; + std::vector descriptorWrites; + std::vector shaderStages; std::vector shaderModules; @@ -126,7 +124,6 @@ class Shader final bool isCompute = false; - std::unordered_map uniformInfos; UniformInfo *builtinUniformInfo[BUILTIN_MAX_ENUM]; std::unique_ptr uniformBufferObjectBuffer; diff --git a/src/modules/graphics/vulkan/StreamBuffer.cpp b/src/modules/graphics/vulkan/StreamBuffer.cpp index 8c37de985..517a69eaa 100644 --- a/src/modules/graphics/vulkan/StreamBuffer.cpp +++ b/src/modules/graphics/vulkan/StreamBuffer.cpp @@ -96,6 +96,11 @@ ptrdiff_t StreamBuffer::getHandle() const return (ptrdiff_t) buffer; } +size_t StreamBuffer::getGPUReadOffset() const +{ + return (frameIndex * bufferSize) + frameGPUReadOffset; +} + love::graphics::StreamBuffer::MapInfo StreamBuffer::map(size_t /*minsize*/) { // TODO: do we also need to wait until a fence is complete, here? diff --git a/src/modules/graphics/vulkan/StreamBuffer.h b/src/modules/graphics/vulkan/StreamBuffer.h index 6b2c11772..c32533d13 100644 --- a/src/modules/graphics/vulkan/StreamBuffer.h +++ b/src/modules/graphics/vulkan/StreamBuffer.h @@ -47,6 +47,7 @@ class StreamBuffer final virtual void unloadVolatile() override; + size_t getGPUReadOffset() const override; MapInfo map(size_t minsize) override; size_t unmap(size_t usedSize) override; void markUsed(size_t usedSize) override; diff --git a/src/modules/graphics/vulkan/Texture.cpp b/src/modules/graphics/vulkan/Texture.cpp index 229e6bc50..a728b0212 100644 --- a/src/modules/graphics/vulkan/Texture.cpp +++ b/src/modules/graphics/vulkan/Texture.cpp @@ -112,7 +112,7 @@ bool Texture::loadVolatile() } } - if (texType == TEXTURE_CUBE || texType == TEXTURE_2D_ARRAY) + if (texType == TEXTURE_CUBE || (texType == TEXTURE_2D_ARRAY && layerCount >= 6)) createFlags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; msaaSamples = vgfx->getMsaaCount(requestedMSAA); diff --git a/src/modules/graphics/wrap_Shader.cpp b/src/modules/graphics/wrap_Shader.cpp index 69a9ee941..51de56b8c 100644 --- a/src/modules/graphics/wrap_Shader.cpp +++ b/src/modules/graphics/wrap_Shader.cpp @@ -446,7 +446,7 @@ int w_Shader_send(lua_State *L) const char *name = luaL_checkstring(L, 2); const Shader::UniformInfo *info = shader->getUniformInfo(name); - if (info == nullptr) + if (info == nullptr || !info->active) return luaL_error(L, "Shader uniform '%s' does not exist.\nA common error is to define but not use the variable.", name); if (luax_istype(L, 3, Data::type) || (info->baseType == Shader::UNIFORM_MATRIX && luax_istype(L, 4, Data::type))) @@ -461,14 +461,11 @@ int w_Shader_sendColors(lua_State *L) const char *name = luaL_checkstring(L, 2); const Shader::UniformInfo *info = shader->getUniformInfo(name); - if (info == nullptr) - { - luax_pushboolean(L, false); - return 1; - } + if (info == nullptr || !info->active) + return luaL_error(L, "Shader uniform '%s' does not exist.\nA common error is to define but not use the variable.", name); if (info->baseType != Shader::UNIFORM_FLOAT || info->components < 3) - return luaL_error(L, "sendColor can only be used on vec3 or vec4 uniforms."); + return luaL_error(L, "Shader:sendColor can only be used with vec3 or vec4 uniforms."); if (luax_istype(L, 3, Data::type)) w_Shader_sendData(L, 3, shader, info, true); diff --git a/testing/classes/TestMethod.lua b/testing/classes/TestMethod.lua index f4f8688d6..1f336d06b 100644 --- a/testing/classes/TestMethod.lua +++ b/testing/classes/TestMethod.lua @@ -123,38 +123,6 @@ TestMethod = { end, - -- @method - TestMethod:assertPixels() - -- @desc - checks a list of coloured pixels agaisnt given imgdata - -- @param {ImageData} imgdata - image data to check - -- @param {table} pixelchecks - map of colors to list of pixel coords, i.e. - -- { blue = { {1, 1}, {2, 2}, {3, 4} } } - -- @return {nil} - assertPixels = function(self, imgdata, pixelchecks, label) - for i, v in pairs(pixelchecks) do - local col = self.colors[i] - local pixels = v - for p=1,#pixels do - local coord = pixels[p] - local tr, tg, tb, ta = imgdata:getPixel(coord[1], coord[2]) - local compare_id = tostring(coord[1]) .. ',' .. tostring(coord[2]) - -- prevent us getting stuff like 0.501960785 for 0.5 red - tr = math.floor((tr*10)+0.5)/10 - tg = math.floor((tg*10)+0.5)/10 - tb = math.floor((tb*10)+0.5)/10 - ta = math.floor((ta*10)+0.5)/10 - col[1] = math.floor((col[1]*10)+0.5)/10 - col[2] = math.floor((col[2]*10)+0.5)/10 - col[3] = math.floor((col[3]*10)+0.5)/10 - col[4] = math.floor((col[4]*10)+0.5)/10 - self:assertEquals(col[1], tr, 'check pixel r for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')') - self:assertEquals(col[2], tg, 'check pixel g for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')') - self:assertEquals(col[3], tb, 'check pixel b for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')') - self:assertEquals(col[4], ta, 'check pixel a for ' .. i .. ' at ' .. compare_id .. '(' .. label .. ')') - end - end - end, - - -- @method - TestMethod:assertRange() -- @desc - used to check a value is within an expected range -- @param {number} actual - actual value of the test @@ -302,10 +270,11 @@ TestMethod = { -- @param {table} imgdata - imgdata to save as a png -- @return {nil} compareImg = function(self, imgdata) - local expected = love.image.newImageData( - 'tempoutput/expected/love.test.graphics.' .. self.method .. '-' .. - tostring(self.imgs) .. '.png' - ) + local expected_path = 'tempoutput/expected/love.test.graphics.' .. + self.method .. '-' .. tostring(self.imgs) .. '.png' + local ok, chunk, _ = pcall(love.image.newImageData, expected_path) + if ok == false then return self:assertEquals(true, false, chunk) end + local expected = chunk local iw = imgdata:getWidth()-2 local ih = imgdata:getHeight()-2 local rgba_tolerance = self.rgba_tolerance * (1/255) @@ -350,13 +319,30 @@ TestMethod = { ) end end - local path = 'tempoutput/actual/love.test.graphics.' .. + local path = 'tempoutput/actual/love.test.graphics.' .. self.method .. '-' .. tostring(self.imgs) .. '.png' imgdata:encode('png', path) self.imgs = self.imgs + 1 end, + -- @method - TestMethod:exportImg() + -- @desc - exports the given imgdata to the 'output/expected/' folder, to use when + -- writing new graphics tests to set the expected image output + -- @NOTE - you should not leave this method in when you are finished this is + -- for test writing only + -- @param {table} imgdata - imgdata to save as a png + -- @param {integer} imgdata - index of the png, graphic tests are run sequentially + -- and each test image is numbered in order that its + -- compared to, so set the number here to match + -- @return {nil} + exportImg = function(self, imgdata, index) + local path = 'tempoutput/expected/love.test.graphics.' .. + self.method .. '-' .. tostring(index) .. '.png' + imgdata:encode('png', path) + end, + + -- @method - TestMethod:skipTest() -- @desc - used to mark this test as skipped for a specific reason -- @param {string} reason - reason why method is being skipped @@ -367,11 +353,19 @@ TestMethod = { end, + -- @method - TestMethod:waitFrames() + -- @desc - yields the method for x amount of frames + -- @param {number} frames - no. frames to wait + -- @return {nil} waitFrames = function(self, frames) - for i=1,frames do coroutine.yield() end + for _=1,frames do coroutine.yield() end end, + -- @method - TestMethod:waitSeconds() + -- @desc - yields the method for x amount of seconds + -- @param {number} seconds - no. seconds to wait + -- @return {nil} waitSeconds = function(self, seconds) local start = love.timer.getTime() while love.timer.getTime() < start + seconds do diff --git a/testing/conf.lua b/testing/conf.lua index c893a2a53..6dc92770f 100644 --- a/testing/conf.lua +++ b/testing/conf.lua @@ -1,4 +1,5 @@ function love.conf(t) + print("love.conf") t.console = true t.window.name = 'love.test' t.window.width = 360 @@ -7,3 +8,13 @@ function love.conf(t) t.window.depth = true t.window.stencil = true end + +-- custom crash message here to catch anything that might occur with modules +-- loading before we hit main.lua +local function error_printer(msg, layer) + print((debug.traceback("Error: " .. tostring(msg), 1+(layer or 1)):gsub("\n[^\n]+$", ""))) +end +function love.errorhandler(msg) + msg = tostring(msg) + error_printer(msg, 2) +end diff --git a/testing/output/expected/love.test.graphics.scale-1.png b/testing/output/expected/love.test.graphics.scale-1.png new file mode 100644 index 000000000..a97348e50 Binary files /dev/null and b/testing/output/expected/love.test.graphics.scale-1.png differ diff --git a/testing/readme.md b/testing/readme.md index 9cd0cc254..f6902067d 100644 --- a/testing/readme.md +++ b/testing/readme.md @@ -82,7 +82,6 @@ Each module has a TestModule object created, and each test method has a TestMeth - **assertGreaterEqual**(expected, actual, label) - **assertLessEqual**(expected, actual, label) - **assertObject**(table) -- **assertPixels**(imgdata, pixeltable, label) - **assertCoords**(expected, actual, label) Example test method: @@ -114,14 +113,7 @@ For sanity-checking, if it's currently not covered or it's not possible to test ## Todo If you would like to contribute to the test suite please raise a PR with the main [love-test](https://github.com/ellraiser/love-test) repo. -The following items are all the things still outstanding, expanding on any existing tests is also very welcome! -- [ ] check for any 12.0 methods in the changelog not yet covered in the test suite -- [ ] add BMfont alt. tests for font class tests (Rasterizer + GlyphData) -- [ ] graphics.isCompressed() should have an example of all compressed files -- [ ] graphics.Mesh should have some graphical tests ideally to check vertex settings w/ shaders -- [ ] ability to test loading different combinations of modules if needed -- [ ] more scenario based tests similar to some of the obj class tests -- [ ] performance tests? need to discuss what + how +There is a list of outstanding methods that require test coverage in `todo.md`, expanding on any existing tests is also very welcome! --- @@ -144,7 +136,6 @@ You can specify the test suite is being run on a runner by adding the `--isRunne `& 'c:\Program Files\LOVE\love.exe' PATH_TO_TESTING_FOLDER/main.lua --console --runAllTests --isRunner` | Test | OS | Exception | Reason | | -------------------------- | --------- | ------------------- | ------ | -| love.graphics.points | MacOS | 1px tolerance | Points are offset by 1,1 when drawn | | love.graphics.setWireframe | MacOS | 1px tolerance | Wireframes are offset by 1,1 when drawn | | love.graphica.arc | MacOS | Skipped | Arc curves are drawn slightly off at really low scale | | love.graphics.setLineStyle | Linux | 1rgba tolerance | 'Rough' lines blend differently with the background rgba | diff --git a/testing/resources/pop.ogg b/testing/resources/pop.ogg new file mode 100644 index 000000000..fb0a82a7b Binary files /dev/null and b/testing/resources/pop.ogg differ diff --git a/testing/resources/tone.ogg b/testing/resources/tone.ogg new file mode 100644 index 000000000..1e7156e47 Binary files /dev/null and b/testing/resources/tone.ogg differ diff --git a/testing/tests/audio.lua b/testing/tests/audio.lua index 39133ab91..24eb7ea0a 100644 --- a/testing/tests/audio.lua +++ b/testing/tests/audio.lua @@ -299,6 +299,20 @@ love.test.audio.getOrientation = function(test) end +-- love.audio.getPlaybackDevice +love.test.audio.getPlaybackDevice = function(test) + test:assertNotNil(love.audio.getPlaybackDevice) + test:assertNotNil(love.audio.getPlaybackDevice()) +end + + +-- love.audio.getPlaybackDevices +love.test.audio.getPlaybackDevices = function(test) + test:assertNotNil(love.audio.getPlaybackDevices) + test:assertGreaterEqual(0, #love.audio.getPlaybackDevices(), 'check table') +end + + -- love.audio.getPosition -- @NOTE is there an expected default listener pos? love.test.audio.getPosition = function(test) @@ -442,6 +456,52 @@ love.test.audio.setOrientation = function(test) end +-- love.audio.setPlaybackDevice +love.test.audio.setPlaybackDevice = function(test) + -- check method + test:assertNotNil(love.audio.setPlaybackDevice) + -- check blank string name + local success1, msg1 = love.audio.setPlaybackDevice('') + -- check invalid name + local success2, msg2 = love.audio.setPlaybackDevice('loveFM') + -- check setting already set + local success3, msg3 = love.audio.setPlaybackDevice(love.audio.getPlaybackDevice()) -- current name + -- rn on macos all 3 return false + -- whereas linux/windows return true for blank/current, which is expected + -- as openalsoft treats blank as current + if love.system.getOS() == 'OS X' then + test:assertFalse(success1, 'check blank device fails') + test:assertFalse(success2, 'check invalid device fails') + test:assertFalse(success3, 'check existing device fails') + else + test:assertTrue(success1, 'check blank device is fine') + test:assertFalse(success2, 'check invalid device fails') + test:assertTrue(success3, 'check existing device is fine') + end + -- if other devices to play with lets set a different one + local devices = love.audio.getPlaybackDevices() + if #devices > 1 then + local another = '' + local current = love.audio.getPlaybackDevice() + for a=1,#devices do + if devices[a] ~= current then + another = devices[a] + break + end + end + if another ~= '' then + -- check setting new device + local success4, msg4 = love.audio.setPlaybackDevice(another) + test:assertTrue(success4, 'check setting different device') + -- check resetting to default + local success5, msg5 = love.audio.setPlaybackDevice() + test:assertTrue(success5, 'check resetting') + test:assertEquals(current, love.audio.getPlaybackDevice()) + end + end +end + + -- love.audio.setPosition love.test.audio.setPosition = function(test) -- check setting position vals are returned diff --git a/testing/tests/data.lua b/testing/tests/data.lua index 21b66b609..073ca63fa 100644 --- a/testing/tests/data.lua +++ b/testing/tests/data.lua @@ -33,6 +33,10 @@ love.test.data.ByteData = function(test) test:assertEquals('o', byte5) end + -- check overwriting the byte data string + data:setString('love!', 5) + test:assertEquals('hellolove!', data:getString(), 'check change string') + end @@ -79,6 +83,10 @@ love.test.data.compress = function(test) { love.data.compress('string', 'gzip', 'helloworld', -1), 'string'}, { love.data.compress('string', 'gzip', 'helloworld', 0), 'string'}, { love.data.compress('string', 'gzip', 'helloworld', 9), 'string'}, + { love.data.compress('string', 'deflate', 'aaaaaa', 1), 'string'}, + { love.data.compress('string', 'deflate', 'heloworld', -1), 'string'}, + { love.data.compress('string', 'deflate', 'heloworld', 0), 'string'}, + { love.data.compress('string', 'deflate', 'heloworld', 9), 'string'}, { love.data.compress('data', 'lz4', 'helloworld', -1), 'userdata'}, { love.data.compress('data', 'lz4', 'helloworld', 0), 'userdata'}, { love.data.compress('data', 'lz4', 'helloworld', 9), 'userdata'}, @@ -87,7 +95,10 @@ love.test.data.compress = function(test) { love.data.compress('data', 'zlib', 'helloworld', 9), 'userdata'}, { love.data.compress('data', 'gzip', 'helloworld', -1), 'userdata'}, { love.data.compress('data', 'gzip', 'helloworld', 0), 'userdata'}, - { love.data.compress('data', 'gzip', 'helloworld', 9), 'userdata'} + { love.data.compress('data', 'gzip', 'helloworld', 9), 'userdata'}, + { love.data.compress('data', 'deflate', 'heloworld', -1), 'userdata'}, + { love.data.compress('data', 'deflate', 'heloworld', 0), 'userdata'}, + { love.data.compress('data', 'deflate', 'heloworld', 9), 'userdata'}, } for c=1,#compressions do test:assertNotNil(compressions[c][1]) diff --git a/testing/tests/filesystem.lua b/testing/tests/filesystem.lua index 55e112553..c7d76e6f6 100644 --- a/testing/tests/filesystem.lua +++ b/testing/tests/filesystem.lua @@ -187,6 +187,27 @@ love.test.filesystem.getDirectoryItems = function(test) end +-- love.filesystem.getFullCommonPath +love.test.filesystem.getFullCommonPath = function(test) + -- check standard paths + local appsavedir = love.filesystem.getFullCommonPath('appsavedir') + local appdocuments = love.filesystem.getFullCommonPath('appdocuments') + local userhome = love.filesystem.getFullCommonPath('userhome') + local userappdata = love.filesystem.getFullCommonPath('userappdata') + local userdesktop = love.filesystem.getFullCommonPath('userdesktop') + local userdocuments = love.filesystem.getFullCommonPath('userdocuments') + test:assertNotNil(appsavedir) + test:assertNotNil(appdocuments) + test:assertNotNil(userhome) + test:assertNotNil(userappdata) + test:assertNotNil(userdesktop) + test:assertNotNil(userdocuments) + -- check invalid path + local ok = pcall(love.filesystem.getFullCommonPath, 'fakepath') + test:assertFalse(ok, 'check invalid common path') +end + + -- love.filesystem.getIdentity love.test.filesystem.getIdentity = function(test) -- check setting identity matches @@ -263,6 +284,7 @@ love.test.filesystem.getInfo = function(test) test:assertEquals(nil, love.filesystem.getInfo('foo/bar/file2.txt', 'directory'), 'check not directory') test:assertNotEquals(nil, love.filesystem.getInfo('foo/bar/file2.txt'), 'check info not nil') test:assertEquals(love.filesystem.getInfo('foo/bar/file2.txt').size, 5, 'check info size match') + test:assertFalse(love.filesystem.getInfo('foo/bar/file2.txt').readonly, 'check readonly') -- @TODO test modified timestamp from info.modtime? -- cleanup love.filesystem.remove('foo/bar/file2.txt') @@ -300,12 +322,16 @@ love.test.filesystem.load = function(test) love.filesystem.write('test1.lua', 'function test()\nreturn 1\nend\nreturn test()') love.filesystem.write('test2.lua', 'function test()\nreturn 1') -- check file that doesn't exist - local chunk, errormsg = love.filesystem.load('faker.lua') - test:assertEquals(nil, chunk, 'check file doesnt exist') - -- check valid lua file - chunk, errormsg = love.filesystem.load('test1.lua') - test:assertEquals(nil, errormsg, 'check no error message') - test:assertEquals(1, chunk(), 'check lua file runs') + local chunk1, errormsg1 = love.filesystem.load('faker.lua', 'b') + test:assertEquals(nil, chunk1, 'check file doesnt exist') + -- check valid lua file (text load) + local chunk2, errormsg2 = love.filesystem.load('test1.lua', 't') + test:assertEquals(nil, errormsg2, 'check no error message') + test:assertEquals(1, chunk2(), 'check lua file runs') + -- check valid lua file (any load) + local chunk4, errormsg4 = love.filesystem.load('test1.lua', 'bt') + test:assertEquals(nil, errormsg2, 'check no error message') + test:assertEquals(1, chunk4(), 'check lua file runs') -- check invalid lua file local ok, chunk, err = pcall(love.filesystem.load, 'test2.lua') test:assertFalse(ok, 'check invalid lua file') @@ -334,6 +360,88 @@ love.test.filesystem.mount = function(test) end +-- love.filesystem.mountFullPath +love.test.filesystem.mountFullPath = function(test) + -- mount something in the working directory + local mount = love.filesystem.mountFullPath(love.filesystem.getSource() .. '/tests', 'tests', 'read') + test:assertTrue(mount, 'check can mount') + -- check reading file through mounted path label + local contents, _ = love.filesystem.read('tests/audio.lua') + test:assertNotEquals(nil, contents) + local unmount = love.filesystem.unmountFullPath(love.filesystem.getSource() .. '/tests') + test:assertTrue(unmount, 'reset mount') +end + + +-- love.filesystem.unmountFullPath +love.test.filesystem.unmountFullPath = function(test) + -- try unmounting something we never mounted + local unmount1 = love.filesystem.unmountFullPath(love.filesystem.getSource() .. '/faker') + test:assertFalse(unmount1, 'check not mounted to start with') + -- mount something to unmount after + love.filesystem.mountFullPath(love.filesystem.getSource() .. '/tests', 'tests', 'read') + local unmount2 = love.filesystem.unmountFullPath(love.filesystem.getSource() .. '/tests') + test:assertTrue(unmount2, 'check unmounted') +end + + +-- love.filesystem.mountCommonPath +love.test.filesystem.mountCommonPath = function(test) + -- check if we can mount all the expected paths + local mount1 = love.filesystem.mountCommonPath('appsavedir', 'appsavedir', 'readwrite') + local mount2 = love.filesystem.mountCommonPath('appdocuments', 'appdocuments', 'readwrite') + local mount3 = love.filesystem.mountCommonPath('userhome', 'userhome', 'readwrite') + local mount4 = love.filesystem.mountCommonPath('userappdata', 'userappdata', 'readwrite') + -- userdesktop isnt valid on linux + if love.system.getOS() ~= 'Linux' then + local mount5 = love.filesystem.mountCommonPath('userdesktop', 'userdesktop', 'readwrite') + test:assertTrue(mount5, 'check mount userdesktop') + end + local mount6 = love.filesystem.mountCommonPath('userdocuments', 'userdocuments', 'readwrite') + local ok = pcall(love.filesystem.mountCommonPath, 'fakepath', 'fake', 'readwrite') + test:assertTrue(mount1, 'check mount appsavedir') + test:assertTrue(mount2, 'check mount appdocuments') + test:assertTrue(mount3, 'check mount userhome') + test:assertTrue(mount4, 'check mount userappdata') + test:assertTrue(mount6, 'check mount userdocuments') + test:assertFalse(ok, 'check mount invalid common path fails') +end + + +-- love.filesystem.unmountCommonPath +--love.test.filesystem.unmountCommonPath = function(test) +-- -- check unmounting invalid +-- local ok = pcall(love.filesystem.unmountCommonPath, 'fakepath') +-- test:assertFalse(ok, 'check unmount invalid common path') +-- -- check mounting valid paths +-- love.filesystem.mountCommonPath('appsavedir', 'appsavedir', 'read') +-- love.filesystem.mountCommonPath('appdocuments', 'appdocuments', 'read') +-- love.filesystem.mountCommonPath('userhome', 'userhome', 'read') +-- love.filesystem.mountCommonPath('userappdata', 'userappdata', 'read') +-- love.filesystem.mountCommonPath('userdesktop', 'userdesktop', 'read') +-- love.filesystem.mountCommonPath('userdocuments', 'userdocuments', 'read') +-- local unmount1 = love.filesystem.unmountCommonPath('appsavedir') +-- local unmount2 = love.filesystem.unmountCommonPath('appdocuments') +-- local unmount3 = love.filesystem.unmountCommonPath('userhome') +-- local unmount4 = love.filesystem.unmountCommonPath('userappdata') +-- local unmount5 = love.filesystem.unmountCommonPath('userdesktop') +-- local unmount6 = love.filesystem.unmountCommonPath('userdocuments') +-- test:assertTrue(unmount1, 'check unmount appsavedir') +-- test:assertTrue(unmount2, 'check unmount appdocuments') +-- test:assertTrue(unmount3, 'check unmount userhome') +-- test:assertTrue(unmount4, 'check unmount userappdata') +-- test:assertTrue(unmount5, 'check unmount userdesktop') +-- test:assertTrue(unmount6, 'check unmount userdocuments') +-- -- remount or future tests fail +-- love.filesystem.mountCommonPath('appsavedir', 'appsavedir', 'readwrite') +-- love.filesystem.mountCommonPath('appdocuments', 'appdocuments', 'readwrite') +-- love.filesystem.mountCommonPath('userhome', 'userhome', 'readwrite') +-- love.filesystem.mountCommonPath('userappdata', 'userappdata', 'readwrite') +-- love.filesystem.mountCommonPath('userdesktop', 'userdesktop', 'readwrite') +-- love.filesystem.mountCommonPath('userdocuments', 'userdocuments', 'readwrite') +--end + + -- love.filesystem.openFile -- @NOTE this is just basic nil checking, objs have their own test method love.test.filesystem.openFile = function(test) diff --git a/testing/tests/graphics.lua b/testing/tests/graphics.lua index ec20edb01..a7bf8e970 100644 --- a/testing/tests/graphics.lua +++ b/testing/tests/graphics.lua @@ -113,6 +113,8 @@ love.test.graphics.Canvas = function(test) debugname = 'testcanvas' }) test:assertObject(canvas) + test:assertTrue(canvas:isCanvas(), 'check is canvas') + test:assertFalse(canvas:isComputeWritable(), 'check not compute writable') -- check dpi test:assertEquals(love.graphics.getDPIScale(), canvas:getDPIScale(), 'check dpi scale') @@ -190,9 +192,6 @@ love.test.graphics.Canvas = function(test) love.graphics.setColor(1, 1, 1, 1) end) local imgdata1 = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata1, { - red = {{0, 0},{0,99},{99,0},{99,99}}, - }, 'font draw check') test:compareImg(imgdata1) -- check using canvas in love.graphics.draw() @@ -201,9 +200,6 @@ love.test.graphics.Canvas = function(test) love.graphics.draw(canvas, 0, 0) love.graphics.setCanvas() local imgdata2 = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata2, { - red = {{0, 0},{0,99},{99,0},{99,99}}, - }, 'font draw check') test:compareImg(imgdata2) -- check depth samples @@ -216,6 +212,16 @@ love.test.graphics.Canvas = function(test) dcanvas:setDepthSampleMode('equal') test:assertEquals('equal', dcanvas:getDepthSampleMode(), 'check depth sample mode set') + -- check compute writeable (wont work on opengl mac) + if love.graphics.getSupported().glsl4 then + local ccanvas = love.graphics.newCanvas(100, 100, { + type = '2d', + format = 'rgba8', + computewrite = true + }) + test:assertTrue(ccanvas:isComputeWritable()) + end + end @@ -269,9 +275,6 @@ love.test.graphics.Font = function(test) love.graphics.print('Aa', 0, 5) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - white = {{0,3},{4,3},{7,4},{9,4},{10,5},{0,8},{4,8},{10,8}}, - }, 'font draw check') test:compareImg(imgdata) -- check font substitution @@ -285,10 +288,6 @@ love.test.graphics.Font = function(test) love.graphics.print('CD', 0, 9) -- should come from fontcd love.graphics.setCanvas() local imgdata2 = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata2, { - green = {{1,8},{6,8},{2,10},{5,10},{9,10}}, - black = {{9,9},{14,8},{14,10},{14,1},{1,10}} - }, 'font draw check') test:compareImg(imgdata2) end @@ -303,6 +302,8 @@ love.test.graphics.Image = function(test) mipmaps = true }) test:assertObject(image) + test:assertFalse(image:isCanvas(), 'check not canvas') + test:assertFalse(image:isComputeWritable(), 'check not compute writable') -- check dpi test:assertEquals(love.graphics.getDPIScale(), image:getDPIScale(), 'check dpi scale') @@ -775,11 +776,6 @@ love.test.graphics.Quad = function(test) love.graphics.draw(texture, quad, 32, 32) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - white = {{17,31},{31,31},{31,24},{32,32},{46,32},{32,46}}, - lovepink = {{2,31},{31,2}}, - loveblue = {{32,61},{61,32}} - }, 'check quad drawing') test:compareImg(imgdata) end @@ -790,7 +786,7 @@ love.test.graphics.Shader = function(test) -- check valid shader local pixelcode1 = [[ - extern Image tex2; + uniform Image tex2; vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { vec4 texturecolor = Texel(tex2, texture_coords); return texturecolor * color; @@ -803,14 +799,14 @@ love.test.graphics.Shader = function(test) ]] local shader1 = love.graphics.newShader(pixelcode1, vertexcode1, {debugname = 'testshader'}) test:assertObject(shader1) - test:assertEquals('vertex shader:\npixel shader:\n', shader1:getWarnings(), 'check shader valid') + test:assertEquals('', shader1:getWarnings(), 'check shader valid') test:assertFalse(shader1:hasUniform('tex1'), 'check invalid uniform') test:assertTrue(shader1:hasUniform('tex2'), 'check valid uniform') test:assertEquals('testshader', shader1:getDebugName()) -- check invalid shader local pixelcode2 = [[ - extern float ww; + uniform float ww; vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { vec4 texturecolor = Texel(tex, texture_coords); float unused = ww * 3 * color; @@ -823,8 +819,8 @@ love.test.graphics.Shader = function(test) -- check using a shader to draw + sending uniforms -- shader will return a given color if overwrite set to 1, otherwise def. draw local pixelcode3 = [[ - extern vec4 col; - extern float overwrite; + uniform vec4 col; + uniform float overwrite; vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { vec4 texcol = Texel(tex, texture_coords); if (overwrite == 1.0) { @@ -836,7 +832,8 @@ love.test.graphics.Shader = function(test) ]] local shader3 = love.graphics.newShader(pixelcode3, vertexcode1) local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) + love.graphics.push("all") + love.graphics.setCanvas(canvas) -- set color to yellow love.graphics.setColor(1, 1, 0, 1) -- turn shader 'on' and use red to draw @@ -849,16 +846,52 @@ love.test.graphics.Shader = function(test) shader3:send('overwrite', 0) love.graphics.setShader(shader3) love.graphics.rectangle('fill', 8, 8, 8, 8) - love.graphics.setShader() - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() + love.graphics.pop() + local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = {{1,1},{1,7},{7,7},{7,1}}, - yellow = {{8,8},{8,15},{15,15},{15,8}} - }, 'shader draw check') test:compareImg(imgdata) + -- test some uncommon paths for shader uniforms + local shader4 = love.graphics.newShader[[ + uniform bool booleans[5]; + vec4 effect(vec4 vcolor, Image tex, vec2 tc, vec2 pc) { + return booleans[3] ? vec4(0, 1, 0, 0) : vec4(1, 0, 0, 0); + } + ]] + + shader4:send("booleans", false, true, true) + + local shader5 = love.graphics.newShader[[ + uniform sampler2D textures[5]; + vec4 effect(vec4 vcolor, Image tex, vec2 tc, vec2 pc) { + return Texel(textures[2], tc) + Texel(textures[3], tc); + } + ]] + + local canvas2 = love.graphics.newCanvas(1, 1) + love.graphics.setCanvas(canvas2) + love.graphics.clear(0, 0.5, 0, 1) + love.graphics.setCanvas() + + shader5:send("textures", canvas2, canvas2, canvas2, canvas2, canvas2) + + local shader6 = love.graphics.newShader[[ + struct Data { + bool boolValue; + float floatValue; + sampler2D tex; + }; + + uniform Data data[3]; + + vec4 effect(vec4 vcolor, Image tex, vec2 tc, vec2 pc) { + return data[1].boolValue ? Texel(data[0].tex, tc) : vec4(0.0, 0.0, 0.0, 0.0); + } + ]] + + shader6:send("data[1].boolValue", true) + shader6:send("data[0].tex", canvas2) + end @@ -931,9 +964,6 @@ love.test.graphics.SpriteBatch = function(test) love.graphics.draw(sbatch, 0, 0) love.graphics.setCanvas() local imgdata1 = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata1, { - lovepink = {{0,0},{63,2},{0,32},{63,32},{63,0},{63,2}} - }, 'sbatch draw normal') test:compareImg(imgdata1) -- use set to change some sprites @@ -945,11 +975,6 @@ love.test.graphics.SpriteBatch = function(test) love.graphics.draw(sbatch, 0, 0) love.graphics.setCanvas() local imgdata2 = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata2, { - lovepink = {{0,32},{63,32}}, - black = {{0,0},{63,0}}, - white = {{0,1},{63,1},{0,31},{63,31}} - }, 'sbatch draw set') test:compareImg(imgdata2) -- set drawRange and redraw @@ -959,11 +984,6 @@ love.test.graphics.SpriteBatch = function(test) love.graphics.draw(sbatch, 0, 0) love.graphics.setCanvas() local imgdata3 = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata3, { - lovepink = {{0,32},{63,32}}, - black = {{0,0},{63,0},{0,48},{63,48}}, - white = {{0,17},{63,17},{0,31},{63,31}} - }, 'sbatch draw drawrange') test:compareImg(imgdata3) -- clear and redraw @@ -973,9 +993,6 @@ love.test.graphics.SpriteBatch = function(test) love.graphics.draw(sbatch, 0, 0) love.graphics.setCanvas() local imgdata4 = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata4, { - black = {{0,0},{63,0},{0,32},{63,32},{0,63},{63,63}}, - }, 'sbatch draw clear') test:compareImg(imgdata4) -- array texture sbatch @@ -999,11 +1016,6 @@ love.test.graphics.SpriteBatch = function(test) love.graphics.draw(asbatch, 0, 0) love.graphics.setCanvas() local imgdata5 = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata5, { - loveblue = {{31,2},{63,2},{3,30},{3,33},{16,47},{63,47}}, - lovepink = {{17,48},{63,48},{31,61},{63,61}}, - black = {{0,0},{63,0},{63,63},{63,0},{30,2},{30,61}}, - }, 'sbatch draw layers') test:compareImg(imgdata5) end @@ -1045,10 +1057,6 @@ love.test.graphics.Text = function(test) love.graphics.draw(colortext, 0, 10) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - yellow = {{1,9},{8,13},{16,11},{22,10},{25,7},{29,9},{32,13},{34,15}}, - white = {{17,13},{30,12},{38,9},{44,13},{58,13},{8,29},{58,29},{57,37},{5,39},{57,45},{1,55}} - }, 'text draw check') test:compareImg(imgdata) end @@ -1110,10 +1118,6 @@ love.test.graphics.Video = function(test) love.graphics.draw(video, 0, 0) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - black = {{0,0},{495,0},{495,499},{0,499}}, - red = {{499,0},{499,499}} - }, 'video draw') test:compareImg(imgdata) end @@ -1197,19 +1201,6 @@ love.test.graphics.circle = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - white = {{13,8},{18,8},{23,13},{23,18}}, - green = { - {15,12},{16,12},{13,13},{18,13},{12,15},{12,16},{13,18},{18,18}, - {15,19},{16,19},{19,15},{19,16} - }, - black = {{10,0},{21,0},{0,10},{0,21},{31,10},{31,21},{10,31},{21,31}}, - yellow = { - {11,10},{10,11},{8,14},{8,17},{10,20},{11,21},{14,23},{17,23},{20,21}, - {21,20},{23,17},{23,14},{20,10},{21,11},{17,8},{14,8} - }, - red = {{11,0},{20,0},{11,31},{20,31},{0,11},{0,20},{31,20},{31,11}} - }, 'circle') test:compareImg(imgdata) end @@ -1223,9 +1214,6 @@ love.test.graphics.clear = function(test) love.graphics.clear(1, 1, 0, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - yellow = {{0,0},{15,0},{0,15},{15,15},{8,8}} - }, 'clear') test:compareImg(imgdata) end @@ -1258,12 +1246,6 @@ love.test.graphics.draw = function(test) love.graphics.draw(canvas1, transform) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas2) - test:assertPixels(imgdata, { - lovepink = {{23,3},{23,19},{7,19},{0,0},{16,0},{0,16},{16,16}}, - loveblue = {{0,31},{15,17},{15,31},{16,31},{31,17},{31,31},{16,15},{31,15}}, - white = {{6,19},{8,19},{22,19},{24,19},{22,3},{24,3}}, - red = {{0,1},{1,0},{15,0},{15,7},{0,15},{7,15}} - }, 'drawing') test:compareImg(imgdata) end @@ -1284,12 +1266,6 @@ love.test.graphics.drawInstanced = function(test) love.graphics.drawInstanced(mesh, 1000, 0, 0, 0, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = {{0,0}}, - green = {{63,0}}, - blue = {{63,63}}, - yellow = {{0,63}} - }, 'draw instances') -- need 1 tolerance here just cos of the amount of colors test.rgba_tolerance = 1 test:compareImg(imgdata) @@ -1311,12 +1287,6 @@ love.test.graphics.drawLayer = function(test) love.graphics.drawLayer(image, 3, 32, 32, 0, 2, 2, 16, 16) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - lovepink = {{30,2},{33,2},{2,30},{2,33},{4,60},{4,63},{60,4},{63,4},{31,23},{32,23}}, - loveblue = {{14,33},{17,33},{46,1},{49,1},{1,46},{1,49},{33,14},{33,17}}, - black = {{0,0},{63,0},{0,63},{39,6},{40,6},{6,39},{6,40},{6,55},{55,6}}, - white = {{46,11},{48,11},{14,43},{16,43},{30,23},{33,23},{34,54},{53,40},{63,63}} - }, 'draw layer') test:compareImg(imgdata) end @@ -1335,11 +1305,6 @@ love.test.graphics.ellipse = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = {{0,14},{0,17},{7,9},{7,22},{14,15},{14,16}}, - pink = {{15,15},{16,15},{8,0},{8,4},{23,0},{23,4},{13,14},{18,14}}, - yellow = {{24,0},{25,0},{14,17},{14,30},{15,31},{31,8}} - }, 'ellipses') test:compareImg(imgdata) end @@ -1372,10 +1337,6 @@ love.test.graphics.line = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - yellow = {{0,0},{15,0},{0,15},{15,15},{7,7},{8,7},{8,7},{8,8}}, - red = {{1,0},{14,0},{0,1},{0,14},{15,1},{15,14},{1,15},{14,15}} - }, 'lines') test:compareImg(imgdata) end @@ -1383,19 +1344,16 @@ end -- love.graphics.points love.test.graphics.points = function(test) local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) + love.graphics.push("all") + love.graphics.setCanvas(canvas) love.graphics.clear(0, 0, 0, 1) + love.graphics.translate(0.5, 0.5) -- draw points at the center of pixels love.graphics.setColor(1, 0, 0, 1) - love.graphics.points(1,1,16,1,16,16,1,16,1,1) + love.graphics.points(0,0,15,0,15,15,0,15,0,0) love.graphics.setColor(1, 1, 0, 1) - love.graphics.points({2,2,8,8,15,2,8,9,15,15,9,9,2,15,9,8}) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() + love.graphics.points({1,1,7,7,14,1,7,8,14,14,8,8,1,14,8,7}) + love.graphics.pop() local imgdata = love.graphics.readbackTexture(canvas) - -- on macOS runners points are drawn 1px off from the target - if GITHUB_RUNNER and love.system.getOS() == 'OS X' then - test.pixel_tolerance = 1 - end test:compareImg(imgdata) end @@ -1412,10 +1370,6 @@ love.test.graphics.polygon = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - yellow = {{1,0},{1,1},{5,9},{7,14},{8,14},{12,3}}, - red = {{2,1},{1,2},{1,7},{5,15},{14,15},{8,8},{14,2},{7,1}} - }, 'polygon') test:compareImg(imgdata) end @@ -1435,17 +1389,6 @@ love.test.graphics.print = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = {{0,0},{1,0},{1,1},{2,6},{4,4},{7,6},{10,2},{11,5},{14,3},{14,4}}, - green = { - {2,1},{2,2},{0,3},{1,3},{1,8},{2,9},{7,10},{8,8},{9,4},{13,3},{14,2}, - {13,8},{14,9} - }, - blue = { - {4,15},{10,15},{4,12},{6,12},{8,12},{5,9},{7,9},{4,3},{10,3},{8,6},{7,7}, - {4,7},{7,13},{8,12} - } - }, 'print') test:compareImg(imgdata) end @@ -1465,17 +1408,6 @@ love.test.graphics.printf = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = { - {1,0},{1,1},{0,3},{2,3},{2,7},{0,9},{3,11},{4,10},{0,15},{4,15},{2,19}, - {0,24},{1,23},{3,23},{4,24},{0,26},{1,27},{2,27},{3,27} - }, - green = { - {1,2},{0,8},{1,8},{2,8},{4,7},{5,8},{7,8},{8,7},{10,4},{14,4},{11,7}, - {12,8},{10,13},{11,12},{13,12},{14,13},{10,15},{11,16} - }, - blue = {{6,4},{6,10},{9,7},{10,6},{16,9},{18,9},{21,8},{25,8}} - }, 'printf') test:compareImg(imgdata) end @@ -1493,11 +1425,7 @@ love.test.graphics.rectangle = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata1 = love.graphics.readbackTexture(canvas) - -- test, check red bg and blue central square - test:assertPixels(imgdata1, { - red = {{0,0},{15,0},{15,15},{0,15}}, - blue = {{6,6},{9,6},{9,9},{6,9}} - }, 'fill') + test:compareImg(imgdata1) -- clear canvas to do some line testing love.graphics.setCanvas(canvas) love.graphics.clear(0, 0, 0, 1) @@ -1510,17 +1438,6 @@ love.test.graphics.rectangle = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata2 = love.graphics.readbackTexture(canvas) - -- -- check corners and inner corners - test:assertPixels(imgdata2, { - red = {{3,0},{9,0},{3,15,9,15}}, - blue = {{0,0},{2,0},{0,15},{2,15}}, - green = {{10,0},{15,0},{10,15},{15,15}}, - black = { - {1,1},{1,14},{3,1},{9,1},{3,14}, - {9,14},{11,1},{14,1},{11,14},{14,14} - } - }, 'line') - test:compareImg(imgdata1) test:compareImg(imgdata2) end @@ -1539,6 +1456,11 @@ love.test.graphics.captureScreenshot = function(test) -- need to wait until end of the frame for the screenshot test:assertNotNil(love.filesystem.openFile('example-screenshot.png', 'r')) love.filesystem.remove('example-screenshot.png') + -- test callback version + love.graphics.captureScreenshot(function (idata) + test:assertNotEquals(nil, idata, 'check we have image data') + end) + test:waitFrames(10) end @@ -1962,22 +1884,13 @@ love.test.graphics.intersectScissor = function(test) love.graphics.setScissor() love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = {{0,0},{3,3}}, - black ={{4,0},{0,4},{4,4}} - }, 'intersect scissor') test:compareImg(imgdata) end -- love.graphics.isActive love.test.graphics.isActive = function(test) - local name, version, vendor, device = love.graphics.getRendererInfo() - if string.find(name, 'Vulkan') ~= nil then - test:skipTest('love.graphics.isActive() crashes on Vulkan') - else - test:assertTrue(love.graphics.isActive(), 'check graphics is active') -- i mean if you got this far - end + test:assertTrue(love.graphics.isActive(), 'check graphics is active') -- i mean if you got this far end @@ -2058,13 +1971,6 @@ love.test.graphics.setBlendMode = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - -- check the 4 corners - test:assertPixels(imgdata, { - redpale = {{0,0}}, - black = {{15,0}}, - greenhalf = {{15,15}}, - bluefade = {{0,15}} - }, 'blend mode') love.graphics.setBlendMode('alpha', 'alphamultiply') -- reset -- need 1rgba tolerance here on some machines test.rgba_tolerance = 1 @@ -2087,10 +1993,6 @@ love.test.graphics.setCanvas = function(test) love.graphics.setCanvas() test:assertEquals(nil, love.graphics.getCanvas(), 'check no canvas set') local imgdata = love.graphics.readbackTexture(canvas2) - -- check 2nd canvas is red - test:assertPixels(imgdata, { - red = {{0,0},{15,0},{15,15},{0,15}} - }, 'set canvas') test:compareImg(imgdata) end @@ -2118,12 +2020,6 @@ love.test.graphics.setColor = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = {{0,0},{5,0},{10,0},{15,0}}, - yellow = {{0,1},{5,1},{10,1},{15,1}}, - greenhalf = {{0,2},{5,2},{10,2},{15,2}}, - blue = {{0,3},{5,3},{10,3},{15,3}} - }, 'set color') test:compareImg(imgdata) end @@ -2147,9 +2043,6 @@ love.test.graphics.setColorMask = function(test) love.graphics.setColorMask(true, true, true, true) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - yellow = {{0,0},{0,15},{15,15},{15,0}} - }, 'set color mask') test:compareImg(imgdata) end @@ -2195,13 +2088,6 @@ love.test.graphics.setFont = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = { - {0,0},{0,6},{2,6},{6,2}, - {4,4},{8,4},{6,6},{10,2}, - {14,2},{12,6} - } - }, 'set font for print') test:compareImg(imgdata) end @@ -2244,12 +2130,6 @@ love.test.graphics.setLineJoin = function(test) love.graphics.origin() love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - black = {{8,0}}, - red = {{8,4}}, - yellow = {{8,7}}, - blue = {{8,8}} - }, 'set line join') test:compareImg(imgdata) end @@ -2272,10 +2152,6 @@ love.test.graphics.setLineStyle = function(test) love.graphics.origin() love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = {{0,0},{7,0},{15,0}}, - red07 = {{0,4},{7,4},{15,4}} - }, 'set line style') -- linux runner needs a 1/255 tolerance for the blend between a rough line + bg if GITHUB_RUNNER and love.system.getOS() == 'Linux' then test.rgba_tolerance = 1 @@ -2307,12 +2183,6 @@ love.test.graphics.setLineWidth = function(test) love.graphics.origin() love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - black = {{0,2},{6,2},{0,6},{5,6},{0,11},{5,11}}, - red = {{0,0},{0,1},{7,2},{8,2}}, - yellow = {{0,3},{0,5},{6,6},{8,6}}, - blue = {{0,7},{0,10},{6,15},{9,15}} - }, 'set line width') test:compareImg(imgdata) end @@ -2346,10 +2216,6 @@ love.test.graphics.setScissor = function(test) love.graphics.setScissor() love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = {{0,0},{7,0},{0,15},{7,15}}, - black ={{8,0},{8,15},{15,0},{15,15}} - }, 'set scissor') test:compareImg(imgdata) end @@ -2380,9 +2246,6 @@ love.test.graphics.setShader = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - yellow = {{0,0},{15,0},{0,15},{15,15}}, - }, 'check shader set to yellow') test:compareImg(imgdata) end @@ -2401,9 +2264,6 @@ love.test.graphics.setStencilState = function(test) love.graphics.setStencilState() love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { - red = {{6,2},{9,2},{2,6},{2,9},{13,6},{9,6},{6,13},{9,13}} - }, 'check stencil test') test:compareImg(imgdata) end @@ -2455,7 +2315,6 @@ love.test.graphics.applyTransform = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { red = {{10, 0}} }, 'apply transform 10') test:compareImg(imgdata) end @@ -2492,7 +2351,6 @@ love.test.graphics.origin = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { red = {{0, 0}} }, 'origin check') test:compareImg(imgdata) end @@ -2514,7 +2372,6 @@ love.test.graphics.pop = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { red = {{0, 0}} }, 'pop 1') test:compareImg(imgdata) end @@ -2537,7 +2394,6 @@ love.test.graphics.push = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { red = {{1, 1}} }, 'push 1') test:compareImg(imgdata) end @@ -2559,7 +2415,6 @@ love.test.graphics.replaceTransform = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { red = {{10, 0}} }, 'replace transform 10') test:compareImg(imgdata) end @@ -2578,7 +2433,6 @@ love.test.graphics.rotate = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { red = {{0,0},{3,0},{3,3},{0,3}} }, 'rotate 90') test:compareImg(imgdata) end @@ -2596,7 +2450,7 @@ love.test.graphics.scale = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { red = {{1,1},{1,15},{15,1},{15,15}} }, 'scale 4x') + test:compareImg(imgdata) end @@ -2614,7 +2468,7 @@ love.test.graphics.shear = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata1 = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata1, { red = {{1,0},{4,0},{7,3},{10,3}} }, 'shear x') + test:compareImg(imgdata1) -- same again at 0,0, we shear by 2y and then draw -- we can then check the drawn rectangle has moved down love.graphics.setCanvas(canvas) @@ -2626,8 +2480,6 @@ love.test.graphics.shear = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata2 = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata2, { red = { {0,1},{0,4},{3,7},{3,10}} }, 'shear y') - test:compareImg(imgdata1) test:compareImg(imgdata2) end @@ -2665,7 +2517,6 @@ love.test.graphics.translate = function(test) love.graphics.setColor(1, 1, 1, 1) love.graphics.setCanvas() local imgdata = love.graphics.readbackTexture(canvas) - test:assertPixels(imgdata, { red = {{5,0},{0,5},{5,5},{0,0}} }, 'translate 4x') test:compareImg(imgdata) end diff --git a/testing/tests/image.lua b/testing/tests/image.lua index fa9f4cbd2..db98882d4 100644 --- a/testing/tests/image.lua +++ b/testing/tests/image.lua @@ -32,6 +32,11 @@ love.test.image.CompressedImageData = function(test) -- check mipmap count test:assertEquals(7, idata:getMipmapCount(), 'check mipmap count') + -- check linear + test:assertFalse(idata:isLinear(), 'check not linear') + idata:setLinear(true) + test:assertTrue(idata:isLinear(), 'check now linear') + end @@ -78,12 +83,24 @@ love.test.image.ImageData = function(test) local r2, g2, b2 = idata:getPixel(25, 25) test:assertEquals(1, r2+g2+b2, 'check set to red') - -- check encoding to an image + -- check encoding to an image (png) idata:encode('png', 'test-encode.png') - local read = love.filesystem.openFile('test-encode.png', 'r') - test:assertNotNil(read) + local read1 = love.filesystem.openFile('test-encode.png', 'r') + test:assertNotNil(read1) love.filesystem.remove('test-encode.png') + -- check encoding to an image (exr) + local edata = love.image.newImageData(100, 100, 'r16f') + edata:encode('exr', 'test-encode.exr') + local read2 = love.filesystem.openFile('test-encode.exr', 'r') + test:assertNotNil(read2) + love.filesystem.remove('test-encode.exr') + + -- check linear + test:assertFalse(idata:isLinear(), 'check not linear') + idata:setLinear(true) + test:assertTrue(idata:isLinear(), 'check now linear') + end diff --git a/testing/tests/keyboard.lua b/testing/tests/keyboard.lua index e7a4f5630..c8f9748c9 100644 --- a/testing/tests/keyboard.lua +++ b/testing/tests/keyboard.lua @@ -64,6 +64,19 @@ love.test.keyboard.setKeyRepeat = function(test) end +-- love.keyboard.isModifierActive +love.test.keyboard.isModifierActive = function(test) + local active1 = love.keyboard.isModifierActive('numlock') + local active2 = love.keyboard.isModifierActive('capslock') + local active3 = love.keyboard.isModifierActive('scrolllock') + local active4 = love.keyboard.isModifierActive('mode') + test:assertNotNil(active1) + test:assertNotNil(active2) + test:assertNotNil(active3) + test:assertNotNil(active4) +end + + -- love.keyboard.setTextInput love.test.keyboard.setTextInput = function(test) love.keyboard.setTextInput(false) diff --git a/testing/tests/physics.lua b/testing/tests/physics.lua index 974c89a84..5e4662526 100644 --- a/testing/tests/physics.lua +++ b/testing/tests/physics.lua @@ -19,6 +19,12 @@ love.test.physics.Body = function(test) love.physics.newRectangleShape(body2, 5, 5, 10, 10) test:assertObject(body1) + -- check shapes + test:assertEquals(1, #body1:getShapes(), 'check shapes total 1') + test:assertEquals(1, #body2:getShapes(), 'check shapes total 2') + test:assertNotEquals(nil, body1:getShape(), 'check shape 1') + test:assertNotEquals(nil, body2:getShape(), 'check shape 2') + -- check body active test:assertTrue(body1:isActive(), 'check active by def') @@ -135,6 +141,7 @@ love.test.physics.Body = function(test) test:assertRange(y5, 5, 6, 'check mass data reset y') test:assertRange(mass5, 0.1, 0.2, 'check mass data reset mass') test:assertRange(inertia5, 5, 6, 'check mass data reset inertia') + test:assertFalse(body1:hasCustomMassData()) -- check position local x6, y6 = body1:getPosition() @@ -515,6 +522,10 @@ love.test.physics.World = function(test) test:assertRange(world:getBodies()[1]:getY(), 9, 11, 'check body prop change y') test:assertEquals(1, world:getBodyCount(), 'check 1 body count') + -- check shapes in world + test:assertEquals(1, #world:getShapesInArea(0, 0, 10, 10), 'check shapes in area #1') + test:assertEquals(0, #world:getShapesInArea(20, 20, 30, 30), 'check shapes in area #1') + -- check world status test:assertFalse(world:isLocked(), 'check not updating') test:assertFalse(world:isSleepingAllowed(), 'check no sleep (till brooklyn)') @@ -571,6 +582,8 @@ love.test.physics.World = function(test) return 1 end) test:assertEquals(3, shapes, 'check shapes in raycast') + test:assertEquals(world:rayCastClosest(0, 0, 200, 200), rectangle1, 'check closest raycast') + test:assertNotEquals(nil, world:rayCastAny(0, 0, 200, 200), 'check any raycast') -- change collision logic test:assertEquals(nil, world:getContactFilter(), 'check def filter') diff --git a/testing/tests/sound.lua b/testing/tests/sound.lua index 0304d0562..d5032cb30 100644 --- a/testing/tests/sound.lua +++ b/testing/tests/sound.lua @@ -80,6 +80,18 @@ love.test.sound.SoundData = function(test) sdata:setSample(0.002, 1) test:assertEquals(1, sdata:getSample(0.002), 'check setting sample manually') + -- check copying from another sound + local copy1 = love.sound.newSoundData('resources/tone.ogg') + local copy2 = love.sound.newSoundData('resources/pop.ogg') + local before = copy2:getSample(0.02) + copy2:copyFrom(copy1, 0.01, 1, 0.02) + test:assertNotEquals(before, copy2:getSample(0.02), 'check changed') + + -- check slicing + local count = math.floor(copy1:getSampleCount()/2) + local slice = copy1:slice(0, count) + test:assertEquals(count, slice:getSampleCount(), 'check slice length') + end diff --git a/testing/tests/system.lua b/testing/tests/system.lua index e8ef4229c..88912d525 100644 --- a/testing/tests/system.lua +++ b/testing/tests/system.lua @@ -29,6 +29,14 @@ love.test.system.getOS = function(test) end +-- love.system.getPreferredLocales +love.test.system.getPreferredLocales = function(test) + local locale = love.system.getPreferredLocales() + test:assertNotNil(locale) + test:assertEquals('table', type(locale), 'check returns table') +end + + -- love.system.getPowerInfo love.test.system.getPowerInfo = function(test) -- check battery state is one of the documented states diff --git a/testing/tests/window.lua b/testing/tests/window.lua index b1c993776..98cfc2f43 100644 --- a/testing/tests/window.lua +++ b/testing/tests/window.lua @@ -8,6 +8,13 @@ -------------------------------------------------------------------------------- +-- love.window.focus +love.test.window.focus = function(test) + -- cant test as doesnt return anything + test:assertEquals('function', type(love.window.focus), 'check method exists') +end + + -- love.window.fromPixels love.test.window.fromPixels = function(test) -- check dpi/pixel ratio as expected @@ -352,18 +359,3 @@ love.test.window.updateMode = function(test) resizable = true }) end - --- love.window.close --- calling love.window.close() last as it will stop the main test image drawing -love.test.window.z_close = function(test) - -- closing window should cause graphics to not be active - love.window.close() - local active = love.graphics.isActive() - test:assertFalse(active, 'check window not active') - -- should also mark the window as not open and not visible - test:assertFalse(love.window.isOpen(), 'check window closed') - test:assertFalse(love.window.isVisible(), 'check window not visible') - love.window.updateMode(360, 240) -- reset - active = love.graphics.isActive() - test:assertTrue(active, 'check window active again') -end diff --git a/testing/todo.md b/testing/todo.md new file mode 100644 index 000000000..e5f5789ae --- /dev/null +++ b/testing/todo.md @@ -0,0 +1,32 @@ +# TODO +These are all the outstanding methods that require test coverage, along with a few bits that still need doing / discussion. + +## General +- ability to test loading different combinations of modules if needed? +- performance tests? need to discuss what + how, might be better as a seperate thing +- check expected behaviour of mount + unmount with common path + try uncommenting love.filesystem.unmountCommonPath and you'll see the issues +- revisit love.audio.setPlaybackDevice when we update openal soft for MacOS + +## Graphics +- love.graphics.copyBuffer() +- love.graphics.copyBufferToTexture() +- love.graphics.copyTextureToBuffer() +- love.graphics.readbackTexture() +- love.graphics.readbackTextureAsync() +- love.graphics.readbackBuffer() +- love.graphics.readbackBufferAsync() +- love.graphics.newComputeShader() +- love.graphics.dispatchThreadgroups() +- love.graphics.dispatchIndirect() +- love.graphics.newTexture() +- love.graphics.drawFromShader() +- love.graphics.drawFromShaderIndirect() +- love.graphics.drawIndirect() +- love.graphics.getQuadIndexBuffer() +- love.graphics.setBlendState() +- love.graphics.setOrthoProjection() +- love.graphics.setPerspectiveProjection() +- love.graphics.resetProjection() +- love.graphics.Mesh:getAttachedAttributes() +- love.graphics.Shader:hasStage()