diff options
Diffstat (limited to 'modules')
48 files changed, 2459 insertions, 258 deletions
diff --git a/modules/betsy/SCsub b/modules/betsy/SCsub index 9930e1f4cf..ed5dcbf58b 100644 --- a/modules/betsy/SCsub +++ b/modules/betsy/SCsub @@ -4,6 +4,7 @@ Import("env_modules") env_betsy = env_modules.Clone() env_betsy.GLSL_HEADER("bc6h.glsl") +env_betsy.GLSL_HEADER("bc1.glsl") env_betsy.Depends(Glob("*.glsl.gen.h"), ["#glsl_builders.py"]) # Thirdparty source files diff --git a/modules/betsy/bc1.glsl b/modules/betsy/bc1.glsl new file mode 100644 index 0000000000..f1b2c28254 --- /dev/null +++ b/modules/betsy/bc1.glsl @@ -0,0 +1,483 @@ +#[versions] + +standard = ""; +dithered = "#define BC1_DITHER"; + +#[compute] +#version 450 + +#include "CrossPlatformSettings_piece_all.glsl" +#include "UavCrossPlatform_piece_all.glsl" + +#define FLT_MAX 340282346638528859811704183484516925440.0f + +layout(binding = 0) uniform sampler2D srcTex; +layout(binding = 1, rg32ui) uniform restrict writeonly uimage2D dstTexture; + +layout(std430, binding = 2) readonly restrict buffer globalBuffer { + float2 c_oMatch5[256]; + float2 c_oMatch6[256]; +}; + +layout(push_constant, std430) uniform Params { + uint p_numRefinements; + uint p_padding[3]; +} +params; + +layout(local_size_x = 8, // + local_size_y = 8, // + local_size_z = 1) in; + +float3 rgb565to888(float rgb565) { + float3 retVal; + retVal.x = floor(rgb565 / 2048.0f); + retVal.y = floor(mod(rgb565, 2048.0f) / 32.0f); + retVal.z = floor(mod(rgb565, 32.0f)); + + // This is the correct 565 to 888 conversion: + // rgb = floor( rgb * ( 255.0f / float3( 31.0f, 63.0f, 31.0f ) ) + 0.5f ) + // + // However stb_dxt follows a different one: + // rb = floor( rb * ( 256 / 32 + 8 / 32 ) ); + // g = floor( g * ( 256 / 64 + 4 / 64 ) ); + // + // I'm not sure exactly why but it's possible this is how the S3TC specifies it should be decoded + // It's quite possible this is the reason: + // http://www.ludicon.com/castano/blog/2009/03/gpu-dxt-decompression/ + // + // Or maybe it's just because it's cheap to do with integer shifts. + // Anyway, we follow stb_dxt's conversion just in case + // (gives almost the same result, with 1 or -1 of difference for a very few values) + // + // Perhaps when we make 888 -> 565 -> 888 it doesn't matter + // because they end up mapping to the original number + + return floor(retVal * float3(8.25f, 4.0625f, 8.25f)); +} + +float rgb888to565(float3 rgbValue) { + rgbValue.rb = floor(rgbValue.rb * 31.0f / 255.0f + 0.5f); + rgbValue.g = floor(rgbValue.g * 63.0f / 255.0f + 0.5f); + + return rgbValue.r * 2048.0f + rgbValue.g * 32.0f + rgbValue.b; +} + +// linear interpolation at 1/3 point between a and b, using desired rounding type +float3 lerp13(float3 a, float3 b) { +#ifdef STB_DXT_USE_ROUNDING_BIAS + // with rounding bias + return a + floor((b - a) * (1.0f / 3.0f) + 0.5f); +#else + // without rounding bias + return floor((2.0f * a + b) / 3.0f); +#endif +} + +/// Unpacks a block of 4 colors from two 16-bit endpoints +void EvalColors(out float3 colors[4], float c0, float c1) { + colors[0] = rgb565to888(c0); + colors[1] = rgb565to888(c1); + colors[2] = lerp13(colors[0], colors[1]); + colors[3] = lerp13(colors[1], colors[0]); +} + +/** The color optimization function. (Clever code, part 1) +@param outMinEndp16 [out] + Minimum endpoint, in RGB565 +@param outMaxEndp16 [out] + Maximum endpoint, in RGB565 +*/ +void OptimizeColorsBlock(const uint srcPixelsBlock[16], out float outMinEndp16, out float outMaxEndp16) { + // determine color distribution + float3 avgColor; + float3 minColor; + float3 maxColor; + + avgColor = minColor = maxColor = unpackUnorm4x8(srcPixelsBlock[0]).xyz; + for (int i = 1; i < 16; ++i) { + const float3 currColorUnorm = unpackUnorm4x8(srcPixelsBlock[i]).xyz; + avgColor += currColorUnorm; + minColor = min(minColor, currColorUnorm); + maxColor = max(maxColor, currColorUnorm); + } + + avgColor = round(avgColor * 255.0f / 16.0f); + maxColor *= 255.0f; + minColor *= 255.0f; + + // determine covariance matrix + float cov[6]; + for (int i = 0; i < 6; ++i) + cov[i] = 0; + + for (int i = 0; i < 16; ++i) { + const float3 currColor = unpackUnorm4x8(srcPixelsBlock[i]).xyz * 255.0f; + float3 rgbDiff = currColor - avgColor; + + cov[0] += rgbDiff.r * rgbDiff.r; + cov[1] += rgbDiff.r * rgbDiff.g; + cov[2] += rgbDiff.r * rgbDiff.b; + cov[3] += rgbDiff.g * rgbDiff.g; + cov[4] += rgbDiff.g * rgbDiff.b; + cov[5] += rgbDiff.b * rgbDiff.b; + } + + // convert covariance matrix to float, find principal axis via power iter + for (int i = 0; i < 6; ++i) + cov[i] /= 255.0f; + + float3 vF = maxColor - minColor; + + const int nIterPower = 4; + for (int iter = 0; iter < nIterPower; ++iter) { + const float r = vF.r * cov[0] + vF.g * cov[1] + vF.b * cov[2]; + const float g = vF.r * cov[1] + vF.g * cov[3] + vF.b * cov[4]; + const float b = vF.r * cov[2] + vF.g * cov[4] + vF.b * cov[5]; + + vF.r = r; + vF.g = g; + vF.b = b; + } + + float magn = max3(abs(vF.r), abs(vF.g), abs(vF.b)); + float3 v; + + if (magn < 4.0f) { // too small, default to luminance + v.r = 299.0f; // JPEG YCbCr luma coefs, scaled by 1000. + v.g = 587.0f; + v.b = 114.0f; + } else { + v = trunc(vF * (512.0f / magn)); + } + + // Pick colors at extreme points + float3 minEndpoint, maxEndpoint; + float minDot = FLT_MAX; + float maxDot = -FLT_MAX; + for (int i = 0; i < 16; ++i) { + const float3 currColor = unpackUnorm4x8(srcPixelsBlock[i]).xyz * 255.0f; + const float dotValue = dot(currColor, v); + + if (dotValue < minDot) { + minDot = dotValue; + minEndpoint = currColor; + } + + if (dotValue > maxDot) { + maxDot = dotValue; + maxEndpoint = currColor; + } + } + + outMinEndp16 = rgb888to565(minEndpoint); + outMaxEndp16 = rgb888to565(maxEndpoint); +} + +// The color matching function +uint MatchColorsBlock(const uint srcPixelsBlock[16], float3 color[4]) { + uint mask = 0u; + float3 dir = color[0] - color[1]; + float stops[4]; + + for (int i = 0; i < 4; ++i) + stops[i] = dot(color[i], dir); + + // think of the colors as arranged on a line; project point onto that line, then choose + // next color out of available ones. we compute the crossover points for "best color in top + // half"/"best in bottom half" and then the same inside that subinterval. + // + // relying on this 1d approximation isn't always optimal in terms of euclidean distance, + // but it's very close and a lot faster. + // http://cbloomrants.blogspot.com/2008/12/12-08-08-dxtc-summary.html + + float c0Point = trunc((stops[1] + stops[3]) * 0.5f); + float halfPoint = trunc((stops[3] + stops[2]) * 0.5f); + float c3Point = trunc((stops[2] + stops[0]) * 0.5f); + +#ifndef BC1_DITHER + // the version without dithering is straightforward + for (uint i = 16u; i-- > 0u;) { + const float3 currColor = unpackUnorm4x8(srcPixelsBlock[i]).xyz * 255.0f; + + const float dotValue = dot(currColor, dir); + mask <<= 2u; + + if (dotValue < halfPoint) + mask |= ((dotValue < c0Point) ? 1u : 3u); + else + mask |= ((dotValue < c3Point) ? 2u : 0u); + } +#else + // with floyd-steinberg dithering + float4 ep1 = float4(0, 0, 0, 0); + float4 ep2 = float4(0, 0, 0, 0); + + c0Point *= 16.0f; + halfPoint *= 16.0f; + c3Point *= 16.0f; + + for (uint y = 0u; y < 4u; ++y) { + float ditherDot; + uint lmask, step; + + float3 currColor; + float dotValue; + + currColor = unpackUnorm4x8(srcPixelsBlock[y * 4 + 0]).xyz * 255.0f; + dotValue = dot(currColor, dir); + + ditherDot = (dotValue * 16.0f) + (3 * ep2[1] + 5 * ep2[0]); + if (ditherDot < halfPoint) + step = (ditherDot < c0Point) ? 1u : 3u; + else + step = (ditherDot < c3Point) ? 2u : 0u; + ep1[0] = dotValue - stops[step]; + lmask = step; + + currColor = unpackUnorm4x8(srcPixelsBlock[y * 4 + 1]).xyz * 255.0f; + dotValue = dot(currColor, dir); + + ditherDot = (dotValue * 16.0f) + (7 * ep1[0] + 3 * ep2[2] + 5 * ep2[1] + ep2[0]); + if (ditherDot < halfPoint) + step = (ditherDot < c0Point) ? 1u : 3u; + else + step = (ditherDot < c3Point) ? 2u : 0u; + ep1[1] = dotValue - stops[step]; + lmask |= step << 2u; + + currColor = unpackUnorm4x8(srcPixelsBlock[y * 4 + 2]).xyz * 255.0f; + dotValue = dot(currColor, dir); + + ditherDot = (dotValue * 16.0f) + (7 * ep1[1] + 3 * ep2[3] + 5 * ep2[2] + ep2[1]); + if (ditherDot < halfPoint) + step = (ditherDot < c0Point) ? 1u : 3u; + else + step = (ditherDot < c3Point) ? 2u : 0u; + ep1[2] = dotValue - stops[step]; + lmask |= step << 4u; + + currColor = unpackUnorm4x8(srcPixelsBlock[y * 4 + 2]).xyz * 255.0f; + dotValue = dot(currColor, dir); + + ditherDot = (dotValue * 16.0f) + (7 * ep1[2] + 5 * ep2[3] + ep2[2]); + if (ditherDot < halfPoint) + step = (ditherDot < c0Point) ? 1u : 3u; + else + step = (ditherDot < c3Point) ? 2u : 0u; + ep1[3] = dotValue - stops[step]; + lmask |= step << 6u; + + mask |= lmask << (y * 8u); + { + float4 tmp = ep1; + ep1 = ep2; + ep2 = tmp; + } // swap + } +#endif + + return mask; +} + +// The refinement function. (Clever code, part 2) +// Tries to optimize colors to suit block contents better. +// (By solving a least squares system via normal equations+Cramer's rule) +bool RefineBlock(const uint srcPixelsBlock[16], uint mask, inout float inOutMinEndp16, + inout float inOutMaxEndp16) { + float newMin16, newMax16; + const float oldMin = inOutMinEndp16; + const float oldMax = inOutMaxEndp16; + + if ((mask ^ (mask << 2u)) < 4u) // all pixels have the same index? + { + // yes, linear system would be singular; solve using optimal + // single-color match on average color + float3 rgbVal = float3(8.0f / 255.0f, 8.0f / 255.0f, 8.0f / 255.0f); + for (int i = 0; i < 16; ++i) + rgbVal += unpackUnorm4x8(srcPixelsBlock[i]).xyz; + + rgbVal = floor(rgbVal * (255.0f / 16.0f)); + + newMax16 = c_oMatch5[uint(rgbVal.r)][0] * 2048.0f + // + c_oMatch6[uint(rgbVal.g)][0] * 32.0f + // + c_oMatch5[uint(rgbVal.b)][0]; + newMin16 = c_oMatch5[uint(rgbVal.r)][1] * 2048.0f + // + c_oMatch6[uint(rgbVal.g)][1] * 32.0f + // + c_oMatch5[uint(rgbVal.b)][1]; + } else { + const float w1Tab[4] = { 3, 0, 2, 1 }; + const float prods[4] = { 589824.0f, 2304.0f, 262402.0f, 66562.0f }; + // ^some magic to save a lot of multiplies in the accumulating loop... + // (precomputed products of weights for least squares system, accumulated inside one 32-bit + // register) + + float akku = 0.0f; + uint cm = mask; + float3 at1 = float3(0, 0, 0); + float3 at2 = float3(0, 0, 0); + for (int i = 0; i < 16; ++i, cm >>= 2u) { + const float3 currColor = unpackUnorm4x8(srcPixelsBlock[i]).xyz * 255.0f; + + const uint step = cm & 3u; + const float w1 = w1Tab[step]; + akku += prods[step]; + at1 += currColor * w1; + at2 += currColor; + } + + at2 = 3.0f * at2 - at1; + + // extract solutions and decide solvability + const float xx = floor(akku / 65535.0f); + const float yy = floor(mod(akku, 65535.0f) / 256.0f); + const float xy = mod(akku, 256.0f); + + float2 f_rb_g; + f_rb_g.x = 3.0f * 31.0f / 255.0f / (xx * yy - xy * xy); + f_rb_g.y = f_rb_g.x * 63.0f / 31.0f; + + // solve. + const float3 newMaxVal = clamp(floor((at1 * yy - at2 * xy) * f_rb_g.xyx + 0.5f), + float3(0.0f, 0.0f, 0.0f), float3(31, 63, 31)); + newMax16 = newMaxVal.x * 2048.0f + newMaxVal.y * 32.0f + newMaxVal.z; + + const float3 newMinVal = clamp(floor((at2 * xx - at1 * xy) * f_rb_g.xyx + 0.5f), + float3(0.0f, 0.0f, 0.0f), float3(31, 63, 31)); + newMin16 = newMinVal.x * 2048.0f + newMinVal.y * 32.0f + newMinVal.z; + } + + inOutMinEndp16 = newMin16; + inOutMaxEndp16 = newMax16; + + return oldMin != newMin16 || oldMax != newMax16; +} + +#ifdef BC1_DITHER +/// Quantizes 'srcValue' which is originally in 888 (full range), +/// converting it to 565 and then back to 888 (quantized) +float3 quant(float3 srcValue) { + srcValue = clamp(srcValue, 0.0f, 255.0f); + // Convert 888 -> 565 + srcValue = floor(srcValue * float3(31.0f / 255.0f, 63.0f / 255.0f, 31.0f / 255.0f) + 0.5f); + // Convert 565 -> 888 back + srcValue = floor(srcValue * float3(8.25f, 4.0625f, 8.25f)); + + return srcValue; +} + +void DitherBlock(const uint srcPixBlck[16], out uint dthPixBlck[16]) { + float3 ep1[4] = { float3(0, 0, 0), float3(0, 0, 0), float3(0, 0, 0), float3(0, 0, 0) }; + float3 ep2[4] = { float3(0, 0, 0), float3(0, 0, 0), float3(0, 0, 0), float3(0, 0, 0) }; + + for (uint y = 0u; y < 16u; y += 4u) { + float3 srcPixel, dithPixel; + + srcPixel = unpackUnorm4x8(srcPixBlck[y + 0u]).xyz * 255.0f; + dithPixel = quant(srcPixel + trunc((3 * ep2[1] + 5 * ep2[0]) * (1.0f / 16.0f))); + ep1[0] = srcPixel - dithPixel; + dthPixBlck[y + 0u] = packUnorm4x8(float4(dithPixel * (1.0f / 255.0f), 1.0f)); + + srcPixel = unpackUnorm4x8(srcPixBlck[y + 1u]).xyz * 255.0f; + dithPixel = quant( + srcPixel + trunc((7 * ep1[0] + 3 * ep2[2] + 5 * ep2[1] + ep2[0]) * (1.0f / 16.0f))); + ep1[1] = srcPixel - dithPixel; + dthPixBlck[y + 1u] = packUnorm4x8(float4(dithPixel * (1.0f / 255.0f), 1.0f)); + + srcPixel = unpackUnorm4x8(srcPixBlck[y + 2u]).xyz * 255.0f; + dithPixel = quant( + srcPixel + trunc((7 * ep1[1] + 3 * ep2[3] + 5 * ep2[2] + ep2[1]) * (1.0f / 16.0f))); + ep1[2] = srcPixel - dithPixel; + dthPixBlck[y + 2u] = packUnorm4x8(float4(dithPixel * (1.0f / 255.0f), 1.0f)); + + srcPixel = unpackUnorm4x8(srcPixBlck[y + 3u]).xyz * 255.0f; + dithPixel = quant(srcPixel + trunc((7 * ep1[2] + 5 * ep2[3] + ep2[2]) * (1.0f / 16.0f))); + ep1[3] = srcPixel - dithPixel; + dthPixBlck[y + 3u] = packUnorm4x8(float4(dithPixel * (1.0f / 255.0f), 1.0f)); + + // swap( ep1, ep2 ) + for (uint i = 0u; i < 4u; ++i) { + float3 tmp = ep1[i]; + ep1[i] = ep2[i]; + ep2[i] = tmp; + } + } +} +#endif + +void main() { + uint srcPixelsBlock[16]; + + bool bAllColorsEqual = true; + + // Load the whole 4x4 block + const uint2 pixelsToLoadBase = gl_GlobalInvocationID.xy << 2u; + for (uint i = 0u; i < 16u; ++i) { + const uint2 pixelsToLoad = pixelsToLoadBase + uint2(i & 0x03u, i >> 2u); + const float3 srcPixels0 = OGRE_Load2D(srcTex, int2(pixelsToLoad), 0).xyz; + srcPixelsBlock[i] = packUnorm4x8(float4(srcPixels0, 1.0f)); + bAllColorsEqual = bAllColorsEqual && srcPixelsBlock[0] == srcPixelsBlock[i]; + } + + float maxEndp16, minEndp16; + uint mask = 0u; + + if (bAllColorsEqual) { + const uint3 rgbVal = uint3(unpackUnorm4x8(srcPixelsBlock[0]).xyz * 255.0f); + mask = 0xAAAAAAAAu; + maxEndp16 = + c_oMatch5[rgbVal.r][0] * 2048.0f + c_oMatch6[rgbVal.g][0] * 32.0f + c_oMatch5[rgbVal.b][0]; + minEndp16 = + c_oMatch5[rgbVal.r][1] * 2048.0f + c_oMatch6[rgbVal.g][1] * 32.0f + c_oMatch5[rgbVal.b][1]; + } else { +#ifdef BC1_DITHER + uint ditherPixelsBlock[16]; + // first step: compute dithered version for PCA if desired + DitherBlock(srcPixelsBlock, ditherPixelsBlock); +#else +#define ditherPixelsBlock srcPixelsBlock +#endif + + // second step: pca+map along principal axis + OptimizeColorsBlock(ditherPixelsBlock, minEndp16, maxEndp16); + if (minEndp16 != maxEndp16) { + float3 colors[4]; + EvalColors(colors, maxEndp16, minEndp16); // Note min/max are inverted + mask = MatchColorsBlock(srcPixelsBlock, colors); + } + + // third step: refine (multiple times if requested) + bool bStopRefinement = false; + for (uint i = 0u; i < params.p_numRefinements && !bStopRefinement; ++i) { + const uint lastMask = mask; + + if (RefineBlock(ditherPixelsBlock, mask, minEndp16, maxEndp16)) { + if (minEndp16 != maxEndp16) { + float3 colors[4]; + EvalColors(colors, maxEndp16, minEndp16); // Note min/max are inverted + mask = MatchColorsBlock(srcPixelsBlock, colors); + } else { + mask = 0u; + bStopRefinement = true; + } + } + + bStopRefinement = mask == lastMask || bStopRefinement; + } + } + + // write the color block + if (maxEndp16 < minEndp16) { + const float tmpValue = minEndp16; + minEndp16 = maxEndp16; + maxEndp16 = tmpValue; + mask ^= 0x55555555u; + } + + uint2 outputBytes; + outputBytes.x = uint(maxEndp16) | (uint(minEndp16) << 16u); + outputBytes.y = mask; + + uint2 dstUV = gl_GlobalInvocationID.xy; + imageStore(dstTexture, int2(dstUV), uint4(outputBytes.xy, 0u, 0u)); +} diff --git a/modules/betsy/betsy_bc1.h b/modules/betsy/betsy_bc1.h new file mode 100644 index 0000000000..2274ed0a81 --- /dev/null +++ b/modules/betsy/betsy_bc1.h @@ -0,0 +1,1061 @@ +/**************************************************************************/ +/* betsy_bc1.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef BETSY_BC1_H +#define BETSY_BC1_H + +constexpr const float dxt1_encoding_table[1024] = { + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 2, + 0, + 2, + 0, + 0, + 4, + 2, + 1, + 2, + 1, + 2, + 1, + 3, + 0, + 3, + 0, + 3, + 0, + 3, + 1, + 1, + 5, + 3, + 2, + 3, + 2, + 4, + 0, + 4, + 0, + 4, + 1, + 4, + 1, + 4, + 2, + 4, + 2, + 4, + 2, + 3, + 5, + 5, + 1, + 5, + 1, + 5, + 2, + 4, + 4, + 5, + 3, + 5, + 3, + 5, + 3, + 6, + 2, + 6, + 2, + 6, + 2, + 6, + 3, + 5, + 5, + 6, + 4, + 6, + 4, + 4, + 8, + 7, + 3, + 7, + 3, + 7, + 3, + 7, + 4, + 7, + 4, + 7, + 4, + 7, + 5, + 5, + 9, + 7, + 6, + 7, + 6, + 8, + 4, + 8, + 4, + 8, + 5, + 8, + 5, + 8, + 6, + 8, + 6, + 8, + 6, + 7, + 9, + 9, + 5, + 9, + 5, + 9, + 6, + 8, + 8, + 9, + 7, + 9, + 7, + 9, + 7, + 10, + 6, + 10, + 6, + 10, + 6, + 10, + 7, + 9, + 9, + 10, + 8, + 10, + 8, + 8, + 12, + 11, + 7, + 11, + 7, + 11, + 7, + 11, + 8, + 11, + 8, + 11, + 8, + 11, + 9, + 9, + 13, + 11, + 10, + 11, + 10, + 12, + 8, + 12, + 8, + 12, + 9, + 12, + 9, + 12, + 10, + 12, + 10, + 12, + 10, + 11, + 13, + 13, + 9, + 13, + 9, + 13, + 10, + 12, + 12, + 13, + 11, + 13, + 11, + 13, + 11, + 14, + 10, + 14, + 10, + 14, + 10, + 14, + 11, + 13, + 13, + 14, + 12, + 14, + 12, + 12, + 16, + 15, + 11, + 15, + 11, + 15, + 11, + 15, + 12, + 15, + 12, + 15, + 12, + 15, + 13, + 13, + 17, + 15, + 14, + 15, + 14, + 16, + 12, + 16, + 12, + 16, + 13, + 16, + 13, + 16, + 14, + 16, + 14, + 16, + 14, + 15, + 17, + 17, + 13, + 17, + 13, + 17, + 14, + 16, + 16, + 17, + 15, + 17, + 15, + 17, + 15, + 18, + 14, + 18, + 14, + 18, + 14, + 18, + 15, + 17, + 17, + 18, + 16, + 18, + 16, + 16, + 20, + 19, + 15, + 19, + 15, + 19, + 15, + 19, + 16, + 19, + 16, + 19, + 16, + 19, + 17, + 17, + 21, + 19, + 18, + 19, + 18, + 20, + 16, + 20, + 16, + 20, + 17, + 20, + 17, + 20, + 18, + 20, + 18, + 20, + 18, + 19, + 21, + 21, + 17, + 21, + 17, + 21, + 18, + 20, + 20, + 21, + 19, + 21, + 19, + 21, + 19, + 22, + 18, + 22, + 18, + 22, + 18, + 22, + 19, + 21, + 21, + 22, + 20, + 22, + 20, + 20, + 24, + 23, + 19, + 23, + 19, + 23, + 19, + 23, + 20, + 23, + 20, + 23, + 20, + 23, + 21, + 21, + 25, + 23, + 22, + 23, + 22, + 24, + 20, + 24, + 20, + 24, + 21, + 24, + 21, + 24, + 22, + 24, + 22, + 24, + 22, + 23, + 25, + 25, + 21, + 25, + 21, + 25, + 22, + 24, + 24, + 25, + 23, + 25, + 23, + 25, + 23, + 26, + 22, + 26, + 22, + 26, + 22, + 26, + 23, + 25, + 25, + 26, + 24, + 26, + 24, + 24, + 28, + 27, + 23, + 27, + 23, + 27, + 23, + 27, + 24, + 27, + 24, + 27, + 24, + 27, + 25, + 25, + 29, + 27, + 26, + 27, + 26, + 28, + 24, + 28, + 24, + 28, + 25, + 28, + 25, + 28, + 26, + 28, + 26, + 28, + 26, + 27, + 29, + 29, + 25, + 29, + 25, + 29, + 26, + 28, + 28, + 29, + 27, + 29, + 27, + 29, + 27, + 30, + 26, + 30, + 26, + 30, + 26, + 30, + 27, + 29, + 29, + 30, + 28, + 30, + 28, + 30, + 28, + 31, + 27, + 31, + 27, + 31, + 27, + 31, + 28, + 31, + 28, + 31, + 28, + 31, + 29, + 31, + 29, + 31, + 30, + 31, + 30, + 31, + 30, + 31, + 31, + 31, + 31, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 2, + 0, + 2, + 1, + 3, + 0, + 3, + 0, + 3, + 1, + 4, + 0, + 4, + 0, + 4, + 1, + 5, + 0, + 5, + 1, + 6, + 0, + 6, + 0, + 6, + 1, + 7, + 0, + 7, + 0, + 7, + 1, + 8, + 0, + 8, + 1, + 8, + 1, + 8, + 2, + 9, + 1, + 9, + 2, + 9, + 2, + 9, + 3, + 10, + 2, + 10, + 3, + 10, + 3, + 10, + 4, + 11, + 3, + 11, + 4, + 11, + 4, + 11, + 5, + 12, + 4, + 12, + 5, + 12, + 5, + 12, + 6, + 13, + 5, + 13, + 6, + 8, + 16, + 13, + 7, + 14, + 6, + 14, + 7, + 9, + 17, + 14, + 8, + 15, + 7, + 15, + 8, + 11, + 16, + 15, + 9, + 15, + 10, + 16, + 8, + 16, + 9, + 16, + 10, + 15, + 13, + 17, + 9, + 17, + 10, + 17, + 11, + 15, + 16, + 18, + 10, + 18, + 11, + 18, + 12, + 16, + 16, + 19, + 11, + 19, + 12, + 19, + 13, + 17, + 17, + 20, + 12, + 20, + 13, + 20, + 14, + 19, + 16, + 21, + 13, + 21, + 14, + 21, + 15, + 20, + 17, + 22, + 14, + 22, + 15, + 25, + 10, + 22, + 16, + 23, + 15, + 23, + 16, + 26, + 11, + 23, + 17, + 24, + 16, + 24, + 17, + 27, + 12, + 24, + 18, + 25, + 17, + 25, + 18, + 28, + 13, + 25, + 19, + 26, + 18, + 26, + 19, + 29, + 14, + 26, + 20, + 27, + 19, + 27, + 20, + 30, + 15, + 27, + 21, + 28, + 20, + 28, + 21, + 28, + 21, + 28, + 22, + 29, + 21, + 29, + 22, + 24, + 32, + 29, + 23, + 30, + 22, + 30, + 23, + 25, + 33, + 30, + 24, + 31, + 23, + 31, + 24, + 27, + 32, + 31, + 25, + 31, + 26, + 32, + 24, + 32, + 25, + 32, + 26, + 31, + 29, + 33, + 25, + 33, + 26, + 33, + 27, + 31, + 32, + 34, + 26, + 34, + 27, + 34, + 28, + 32, + 32, + 35, + 27, + 35, + 28, + 35, + 29, + 33, + 33, + 36, + 28, + 36, + 29, + 36, + 30, + 35, + 32, + 37, + 29, + 37, + 30, + 37, + 31, + 36, + 33, + 38, + 30, + 38, + 31, + 41, + 26, + 38, + 32, + 39, + 31, + 39, + 32, + 42, + 27, + 39, + 33, + 40, + 32, + 40, + 33, + 43, + 28, + 40, + 34, + 41, + 33, + 41, + 34, + 44, + 29, + 41, + 35, + 42, + 34, + 42, + 35, + 45, + 30, + 42, + 36, + 43, + 35, + 43, + 36, + 46, + 31, + 43, + 37, + 44, + 36, + 44, + 37, + 44, + 37, + 44, + 38, + 45, + 37, + 45, + 38, + 40, + 48, + 45, + 39, + 46, + 38, + 46, + 39, + 41, + 49, + 46, + 40, + 47, + 39, + 47, + 40, + 43, + 48, + 47, + 41, + 47, + 42, + 48, + 40, + 48, + 41, + 48, + 42, + 47, + 45, + 49, + 41, + 49, + 42, + 49, + 43, + 47, + 48, + 50, + 42, + 50, + 43, + 50, + 44, + 48, + 48, + 51, + 43, + 51, + 44, + 51, + 45, + 49, + 49, + 52, + 44, + 52, + 45, + 52, + 46, + 51, + 48, + 53, + 45, + 53, + 46, + 53, + 47, + 52, + 49, + 54, + 46, + 54, + 47, + 57, + 42, + 54, + 48, + 55, + 47, + 55, + 48, + 58, + 43, + 55, + 49, + 56, + 48, + 56, + 49, + 59, + 44, + 56, + 50, + 57, + 49, + 57, + 50, + 60, + 45, + 57, + 51, + 58, + 50, + 58, + 51, + 61, + 46, + 58, + 52, + 59, + 51, + 59, + 52, + 62, + 47, + 59, + 53, + 60, + 52, + 60, + 53, + 60, + 53, + 60, + 54, + 61, + 53, + 61, + 54, + 61, + 54, + 61, + 55, + 62, + 54, + 62, + 55, + 62, + 55, + 62, + 56, + 63, + 55, + 63, + 56, + 63, + 56, + 63, + 57, + 63, + 58, + 63, + 59, + 63, + 59, + 63, + 60, + 63, + 61, + 63, + 62, + 63, + 62, + 63, + 63, +}; + +#endif // BETSY_BC1_H diff --git a/modules/betsy/image_compress_betsy.cpp b/modules/betsy/image_compress_betsy.cpp index bc72203b2f..7b4d8b3dfb 100644 --- a/modules/betsy/image_compress_betsy.cpp +++ b/modules/betsy/image_compress_betsy.cpp @@ -30,39 +30,17 @@ #include "image_compress_betsy.h" -#include "servers/rendering/rendering_device_binds.h" -#include "servers/rendering/rendering_server_default.h" +#include "core/config/project_settings.h" -#if defined(VULKAN_ENABLED) -#include "drivers/vulkan/rendering_context_driver_vulkan.h" -#endif -#if defined(METAL_ENABLED) -#include "drivers/metal/rendering_context_driver_metal.h" -#endif +#include "betsy_bc1.h" +#include "bc1.glsl.gen.h" #include "bc6h.glsl.gen.h" -struct BC6PushConstant { - float sizeX; - float sizeY; - uint32_t padding[2]; -}; - -static int get_next_multiple(int n, int m) { - return n + (m - (n % m)); -} - -Error _compress_betsy(BetsyFormat p_format, Image *r_img) { - uint64_t start_time = OS::get_singleton()->get_ticks_msec(); - - if (r_img->is_compressed()) { - return ERR_INVALID_DATA; - } - - ERR_FAIL_COND_V_MSG(r_img->get_format() < Image::FORMAT_RF || r_img->get_format() > Image::FORMAT_RGBE9995, ERR_INVALID_DATA, "Image is not an HDR image."); - - Error err = OK; +static Mutex betsy_mutex; +static BetsyCompressor *betsy = nullptr; +void BetsyCompressor::_init() { // Create local RD. RenderingContextDriver *rcd = nullptr; RenderingDevice *rd = RenderingServer::get_singleton()->create_local_rendering_device(); @@ -81,7 +59,7 @@ Error _compress_betsy(BetsyFormat p_format, Image *r_img) { #endif #endif if (rcd != nullptr && rd != nullptr) { - err = rcd->initialize(); + Error err = rcd->initialize(); if (err == OK) { err = rd->initialize(rcd); } @@ -95,58 +73,201 @@ Error _compress_betsy(BetsyFormat p_format, Image *r_img) { } } - ERR_FAIL_NULL_V_MSG(rd, err, "Unable to create a local RenderingDevice."); + ERR_FAIL_NULL_MSG(rd, "Unable to create a local RenderingDevice."); + + compress_rd = rd; + compress_rcd = rcd; + + // Create the sampler state. + RD::SamplerState src_sampler_state; + { + src_sampler_state.repeat_u = RD::SAMPLER_REPEAT_MODE_CLAMP_TO_EDGE; + src_sampler_state.repeat_v = RD::SAMPLER_REPEAT_MODE_CLAMP_TO_EDGE; + src_sampler_state.mag_filter = RD::SAMPLER_FILTER_NEAREST; + src_sampler_state.min_filter = RD::SAMPLER_FILTER_NEAREST; + src_sampler_state.mip_filter = RD::SAMPLER_FILTER_NEAREST; + } + + src_sampler = compress_rd->sampler_create(src_sampler_state); +} + +void BetsyCompressor::init() { + WorkerThreadPool::TaskID tid = WorkerThreadPool::get_singleton()->add_task(callable_mp(this, &BetsyCompressor::_thread_loop), true); + command_queue.set_pump_task_id(tid); + command_queue.push(this, &BetsyCompressor::_assign_mt_ids, tid); + command_queue.push_and_sync(this, &BetsyCompressor::_init); + DEV_ASSERT(task_id == tid); +} + +void BetsyCompressor::_assign_mt_ids(WorkerThreadPool::TaskID p_pump_task_id) { + task_id = p_pump_task_id; +} + +// Yield thread to WTP so other tasks can be done on it. +// Automatically regains control as soon a task is pushed to the command queue. +void BetsyCompressor::_thread_loop() { + while (!exit) { + WorkerThreadPool::get_singleton()->yield(); + command_queue.flush_all(); + } +} + +void BetsyCompressor::_thread_exit() { + exit = true; + + if (compress_rd != nullptr) { + if (dxt1_encoding_table_buffer.is_valid()) { + compress_rd->free(dxt1_encoding_table_buffer); + } + + compress_rd->free(src_sampler); + + // Clear the shader cache, pipelines will be unreferenced automatically. + for (KeyValue<String, BetsyShader> &E : cached_shaders) { + if (E.value.compiled.is_valid()) { + compress_rd->free(E.value.compiled); + } + } + cached_shaders.clear(); + } +} + +void BetsyCompressor::finish() { + command_queue.push(this, &BetsyCompressor::_thread_exit); + if (task_id != WorkerThreadPool::INVALID_TASK_ID) { + WorkerThreadPool::get_singleton()->wait_for_task_completion(task_id); + task_id = WorkerThreadPool::INVALID_TASK_ID; + } + + if (compress_rd != nullptr) { + // Free the RD (and RCD if necessary). + memdelete(compress_rd); + compress_rd = nullptr; + if (compress_rcd != nullptr) { + memdelete(compress_rcd); + compress_rcd = nullptr; + } + } +} + +// Helper functions. + +static int get_next_multiple(int n, int m) { + return n + (m - (n % m)); +} + +static String get_shader_name(BetsyFormat p_format) { + switch (p_format) { + case BETSY_FORMAT_BC1: + case BETSY_FORMAT_BC1_DITHER: + return "BC1"; + + case BETSY_FORMAT_BC3: + return "BC3"; + + case BETSY_FORMAT_BC6_SIGNED: + case BETSY_FORMAT_BC6_UNSIGNED: + return "BC6"; + + default: + return ""; + } +} + +Error BetsyCompressor::_compress(BetsyFormat p_format, Image *r_img) { + uint64_t start_time = OS::get_singleton()->get_ticks_msec(); + + if (r_img->is_compressed()) { + return ERR_INVALID_DATA; + } - Ref<RDShaderFile> compute_shader; - compute_shader.instantiate(); + Error err = OK; // Destination format. Image::Format dest_format = Image::FORMAT_MAX; + RD::DataFormat dst_rd_format = RD::DATA_FORMAT_MAX; String version = ""; switch (p_format) { - case BETSY_FORMAT_BC6: { - err = compute_shader->parse_versions_from_text(bc6h_shader_glsl); - - if (r_img->detect_signed(true)) { - dest_format = Image::FORMAT_BPTC_RGBF; - version = "signed"; - } else { - dest_format = Image::FORMAT_BPTC_RGBFU; - version = "unsigned"; - } + case BETSY_FORMAT_BC1: + version = "standard"; + dst_rd_format = RD::DATA_FORMAT_R32G32_UINT; + dest_format = Image::FORMAT_DXT1; + break; + + case BETSY_FORMAT_BC1_DITHER: + version = "dithered"; + dst_rd_format = RD::DATA_FORMAT_R32G32_UINT; + dest_format = Image::FORMAT_DXT1; + break; - } break; + case BETSY_FORMAT_BC6_SIGNED: + version = "signed"; + dst_rd_format = RD::DATA_FORMAT_R32G32B32A32_UINT; + dest_format = Image::FORMAT_BPTC_RGBF; + break; + + case BETSY_FORMAT_BC6_UNSIGNED: + version = "unsigned"; + dst_rd_format = RD::DATA_FORMAT_R32G32B32A32_UINT; + dest_format = Image::FORMAT_BPTC_RGBFU; + break; default: err = ERR_INVALID_PARAMETER; break; } - if (err != OK) { - compute_shader->print_errors("Betsy compress shader"); - memdelete(rd); - if (rcd != nullptr) { - memdelete(rcd); + const String shader_name = get_shader_name(p_format) + "-" + version; + BetsyShader shader; + + if (cached_shaders.has(shader_name)) { + shader = cached_shaders[shader_name]; + + } else { + Ref<RDShaderFile> source; + source.instantiate(); + + switch (p_format) { + case BETSY_FORMAT_BC1: + case BETSY_FORMAT_BC1_DITHER: + err = source->parse_versions_from_text(bc1_shader_glsl); + break; + + case BETSY_FORMAT_BC6_UNSIGNED: + case BETSY_FORMAT_BC6_SIGNED: + err = source->parse_versions_from_text(bc6h_shader_glsl); + break; + + default: + err = ERR_INVALID_PARAMETER; + break; } - return err; - } + if (err != OK) { + source->print_errors("Betsy compress shader"); + return err; + } - // Compile the shader, return early if invalid. - RID shader = rd->shader_create_from_spirv(compute_shader->get_spirv_stages(version)); + // Compile the shader, return early if invalid. + shader.compiled = compress_rd->shader_create_from_spirv(source->get_spirv_stages(version)); + if (shader.compiled.is_null()) { + return ERR_CANT_CREATE; + } - if (shader.is_null()) { - memdelete(rd); - if (rcd != nullptr) { - memdelete(rcd); + // Compile the pipeline, return early if invalid. + shader.pipeline = compress_rd->compute_pipeline_create(shader.compiled); + if (shader.pipeline.is_null()) { + return ERR_CANT_CREATE; } - return err; + cached_shaders[shader_name] = shader; } - RID pipeline = rd->compute_pipeline_create(shader); + if (shader.compiled.is_null() || shader.pipeline.is_null()) { + return ERR_INVALID_DATA; + } // src_texture format information. RD::TextureFormat src_texture_format; @@ -159,6 +280,33 @@ Error _compress_betsy(BetsyFormat p_format, Image *r_img) { } switch (r_img->get_format()) { + case Image::FORMAT_L8: + r_img->convert(Image::FORMAT_RGBA8); + src_texture_format.format = RD::DATA_FORMAT_R8G8B8A8_UNORM; + break; + + case Image::FORMAT_LA8: + r_img->convert(Image::FORMAT_RGBA8); + src_texture_format.format = RD::DATA_FORMAT_R8G8B8A8_UNORM; + break; + + case Image::FORMAT_R8: + src_texture_format.format = RD::DATA_FORMAT_R8_UNORM; + break; + + case Image::FORMAT_RG8: + src_texture_format.format = RD::DATA_FORMAT_R8G8_UNORM; + break; + + case Image::FORMAT_RGB8: + r_img->convert(Image::FORMAT_RGBA8); + src_texture_format.format = RD::DATA_FORMAT_R8G8B8A8_UNORM; + break; + + case Image::FORMAT_RGBA8: + src_texture_format.format = RD::DATA_FORMAT_R8G8B8A8_UNORM; + break; + case Image::FORMAT_RH: src_texture_format.format = RD::DATA_FORMAT_R16_SFLOAT; break; @@ -198,33 +346,23 @@ Error _compress_betsy(BetsyFormat p_format, Image *r_img) { break; default: { - rd->free(shader); - - memdelete(rd); - if (rcd != nullptr) { - memdelete(rcd); - } - return err; } } - // Create the sampler state. - RD::SamplerState src_sampler_state; - { - src_sampler_state.repeat_u = RD::SAMPLER_REPEAT_MODE_CLAMP_TO_EDGE; - src_sampler_state.repeat_v = RD::SAMPLER_REPEAT_MODE_CLAMP_TO_EDGE; - src_sampler_state.mag_filter = RD::SAMPLER_FILTER_NEAREST; - src_sampler_state.min_filter = RD::SAMPLER_FILTER_NEAREST; - src_sampler_state.mip_filter = RD::SAMPLER_FILTER_NEAREST; - } - - RID src_sampler = rd->sampler_create(src_sampler_state); - // For the destination format just copy the source format and change the usage bits. RD::TextureFormat dst_texture_format = src_texture_format; dst_texture_format.usage_bits = RD::TEXTURE_USAGE_COLOR_ATTACHMENT_BIT | RD::TEXTURE_USAGE_STORAGE_BIT | RD::TEXTURE_USAGE_CAN_COPY_FROM_BIT | RD::TEXTURE_USAGE_CAN_COPY_TO_BIT | RD::TEXTURE_USAGE_CAN_UPDATE_BIT; - dst_texture_format.format = RD::DATA_FORMAT_R32G32B32A32_UINT; + dst_texture_format.format = dst_rd_format; + + // Encoding table setup. + if (dest_format == Image::FORMAT_DXT1 && dxt1_encoding_table_buffer.is_null()) { + Vector<uint8_t> data; + data.resize(1024 * 4); + memcpy(data.ptrw(), dxt1_encoding_table, 1024 * 4); + + dxt1_encoding_table_buffer = compress_rd->storage_buffer_create(1024 * 4, data); + } const int mip_count = r_img->get_mipmap_count() + 1; @@ -256,8 +394,41 @@ Error _compress_betsy(BetsyFormat p_format, Image *r_img) { memcpy(src_image_ptr[0].ptrw(), r_img->ptr() + ofs, size); // Create the textures on the GPU. - RID src_texture = rd->texture_create(src_texture_format, RD::TextureView(), src_images); - RID dst_texture = rd->texture_create(dst_texture_format, RD::TextureView()); + RID src_texture = compress_rd->texture_create(src_texture_format, RD::TextureView(), src_images); + RID dst_texture = compress_rd->texture_create(dst_texture_format, RD::TextureView()); + + Vector<RD::Uniform> uniforms; + { + { + RD::Uniform u; + u.uniform_type = RD::UNIFORM_TYPE_SAMPLER_WITH_TEXTURE; + u.binding = 0; + u.append_id(src_sampler); + u.append_id(src_texture); + uniforms.push_back(u); + } + { + RD::Uniform u; + u.uniform_type = RD::UNIFORM_TYPE_IMAGE; + u.binding = 1; + u.append_id(dst_texture); + uniforms.push_back(u); + } + + if (dest_format == Image::FORMAT_DXT1) { + RD::Uniform u; + u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER; + u.binding = 2; + u.append_id(dxt1_encoding_table_buffer); + uniforms.push_back(u); + } + } + + RID uniform_set = compress_rd->uniform_set_create(uniforms, shader.compiled, 0); + RD::ComputeListID compute_list = compress_rd->compute_list_begin(); + + compress_rd->compute_list_bind_compute_pipeline(compute_list, shader.pipeline); + compress_rd->compute_list_bind_uniform_set(compute_list, uniform_set, 0); if (dest_format == Image::FORMAT_BPTC_RGBFU || dest_format == Image::FORMAT_BPTC_RGBF) { BC6PushConstant push_constant; @@ -266,47 +437,33 @@ Error _compress_betsy(BetsyFormat p_format, Image *r_img) { push_constant.padding[0] = 0; push_constant.padding[1] = 0; - Vector<RD::Uniform> uniforms; - { - { - RD::Uniform u; - u.uniform_type = RD::UNIFORM_TYPE_SAMPLER_WITH_TEXTURE; - u.binding = 0; - u.append_id(src_sampler); - u.append_id(src_texture); - uniforms.push_back(u); - } - { - RD::Uniform u; - u.uniform_type = RD::UNIFORM_TYPE_IMAGE; - u.binding = 1; - u.append_id(dst_texture); - uniforms.push_back(u); - } - } + compress_rd->compute_list_set_push_constant(compute_list, &push_constant, sizeof(BC6PushConstant)); - RID uniform_set = rd->uniform_set_create(uniforms, shader, 0); - RD::ComputeListID compute_list = rd->compute_list_begin(); + } else { + BC1PushConstant push_constant; + push_constant.num_refines = 2; + push_constant.padding[0] = 0; + push_constant.padding[1] = 0; + push_constant.padding[2] = 0; - rd->compute_list_bind_compute_pipeline(compute_list, pipeline); - rd->compute_list_bind_uniform_set(compute_list, uniform_set, 0); - rd->compute_list_set_push_constant(compute_list, &push_constant, sizeof(BC6PushConstant)); - rd->compute_list_dispatch(compute_list, get_next_multiple(width, 32) / 32, get_next_multiple(height, 32) / 32, 1); - rd->compute_list_end(); + compress_rd->compute_list_set_push_constant(compute_list, &push_constant, sizeof(BC1PushConstant)); } - rd->submit(); - rd->sync(); + compress_rd->compute_list_dispatch(compute_list, get_next_multiple(width, 32) / 32, get_next_multiple(height, 32) / 32, 1); + compress_rd->compute_list_end(); + + compress_rd->submit(); + compress_rd->sync(); // Copy data from the GPU to the buffer. - const Vector<uint8_t> texture_data = rd->texture_get_data(dst_texture, 0); + const Vector<uint8_t> texture_data = compress_rd->texture_get_data(dst_texture, 0); int64_t dst_ofs = Image::get_image_mipmap_offset(r_img->get_width(), r_img->get_height(), dest_format, i); memcpy(dst_data_ptr + dst_ofs, texture_data.ptr(), texture_data.size()); // Free the source and dest texture. - rd->free(dst_texture); - rd->free(src_texture); + compress_rd->free(dst_texture); + compress_rd->free(src_texture); } src_images.clear(); @@ -314,26 +471,67 @@ Error _compress_betsy(BetsyFormat p_format, Image *r_img) { // Set the compressed data to the image. r_img->set_data(r_img->get_width(), r_img->get_height(), r_img->has_mipmaps(), dest_format, dst_data); - // Free the shader (dependencies will be cleared automatically). - rd->free(src_sampler); - rd->free(shader); - - memdelete(rd); - if (rcd != nullptr) { - memdelete(rcd); - } - print_verbose(vformat("Betsy: Encoding took %d ms.", OS::get_singleton()->get_ticks_msec() - start_time)); return OK; } +void ensure_betsy_exists() { + betsy_mutex.lock(); + if (betsy == nullptr) { + betsy = memnew(BetsyCompressor); + betsy->init(); + } + betsy_mutex.unlock(); +} + Error _betsy_compress_bptc(Image *r_img, Image::UsedChannels p_channels) { + ensure_betsy_exists(); Image::Format format = r_img->get_format(); + Error result = ERR_UNAVAILABLE; if (format >= Image::FORMAT_RF && format <= Image::FORMAT_RGBE9995) { - return _compress_betsy(BETSY_FORMAT_BC6, r_img); + if (r_img->detect_signed()) { + result = betsy->compress(BETSY_FORMAT_BC6_SIGNED, r_img); + } else { + result = betsy->compress(BETSY_FORMAT_BC6_UNSIGNED, r_img); + } + } + + if (!GLOBAL_GET("rendering/textures/vram_compression/cache_gpu_compressor")) { + free_device(); + } + + return result; +} + +Error _betsy_compress_s3tc(Image *r_img, Image::UsedChannels p_channels) { + ensure_betsy_exists(); + Error result = ERR_UNAVAILABLE; + + switch (p_channels) { + case Image::USED_CHANNELS_RGB: + result = betsy->compress(BETSY_FORMAT_BC1_DITHER, r_img); + break; + + case Image::USED_CHANNELS_L: + result = betsy->compress(BETSY_FORMAT_BC1, r_img); + break; + + default: + break; } - return ERR_UNAVAILABLE; + if (!GLOBAL_GET("rendering/textures/vram_compression/cache_gpu_compressor")) { + free_device(); + } + + return result; +} + +void free_device() { + if (betsy != nullptr) { + betsy->finish(); + memdelete(betsy); + } } diff --git a/modules/betsy/image_compress_betsy.h b/modules/betsy/image_compress_betsy.h index a64e586c76..70e4ae85ed 100644 --- a/modules/betsy/image_compress_betsy.h +++ b/modules/betsy/image_compress_betsy.h @@ -32,13 +32,79 @@ #define IMAGE_COMPRESS_BETSY_H #include "core/io/image.h" +#include "core/object/worker_thread_pool.h" +#include "core/os/thread.h" +#include "core/templates/command_queue_mt.h" + +#include "servers/rendering/rendering_device_binds.h" +#include "servers/rendering/rendering_server_default.h" + +#if defined(VULKAN_ENABLED) +#include "drivers/vulkan/rendering_context_driver_vulkan.h" +#endif +#if defined(METAL_ENABLED) +#include "drivers/metal/rendering_context_driver_metal.h" +#endif enum BetsyFormat { - BETSY_FORMAT_BC6, + BETSY_FORMAT_BC1, + BETSY_FORMAT_BC1_DITHER, + BETSY_FORMAT_BC3, + BETSY_FORMAT_BC6_SIGNED, + BETSY_FORMAT_BC6_UNSIGNED, +}; + +struct BC6PushConstant { + float sizeX; + float sizeY; + uint32_t padding[2]; }; -Error _compress_betsy(BetsyFormat p_format, Image *r_img); +struct BC1PushConstant { + uint32_t num_refines; + uint32_t padding[3]; +}; + +void free_device(); Error _betsy_compress_bptc(Image *r_img, Image::UsedChannels p_channels); +Error _betsy_compress_s3tc(Image *r_img, Image::UsedChannels p_channels); + +class BetsyCompressor : public Object { + mutable CommandQueueMT command_queue; + bool exit = false; + WorkerThreadPool::TaskID task_id = WorkerThreadPool::INVALID_TASK_ID; + + struct BetsyShader { + RID compiled; + RID pipeline; + }; + + // Resources shared by all compression formats. + RenderingDevice *compress_rd = nullptr; + RenderingContextDriver *compress_rcd = nullptr; + HashMap<String, BetsyShader> cached_shaders; + RID src_sampler = RID(); + + // Format-specific resources. + RID dxt1_encoding_table_buffer = RID(); + + void _init(); + void _assign_mt_ids(WorkerThreadPool::TaskID p_pump_task_id); + void _thread_loop(); + void _thread_exit(); + + Error _compress(BetsyFormat p_format, Image *r_img); + +public: + void init(); + void finish(); + + Error compress(BetsyFormat p_format, Image *r_img) { + Error err; + command_queue.push_and_ret(this, &BetsyCompressor::_compress, p_format, r_img, &err); + return err; + } +}; #endif // IMAGE_COMPRESS_BETSY_H diff --git a/modules/betsy/register_types.cpp b/modules/betsy/register_types.cpp index 019099e67c..a3a3b5a99b 100644 --- a/modules/betsy/register_types.cpp +++ b/modules/betsy/register_types.cpp @@ -38,10 +38,13 @@ void initialize_betsy_module(ModuleInitializationLevel p_level) { } Image::_image_compress_bptc_rd_func = _betsy_compress_bptc; + Image::_image_compress_bc_rd_func = _betsy_compress_s3tc; } void uninitialize_betsy_module(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } + + free_device(); } diff --git a/modules/gdscript/.editorconfig b/modules/gdscript/.editorconfig index 640c205093..b380846f86 100644 --- a/modules/gdscript/.editorconfig +++ b/modules/gdscript/.editorconfig @@ -1,8 +1,3 @@ [*.gd] -indent_style = tab indent_size = 4 -insert_final_newline = true trim_trailing_whitespace = true - -[*.out] -insert_final_newline = true diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 276a12f5de..7b9aa70686 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -880,6 +880,11 @@ Error GDScript::reload(bool p_keep_state) { if (can_run && p_keep_state) { _restore_old_static_data(); } + + if (p_keep_state) { + // Update the properties in the inspector. + update_exports(); + } #endif reloading = false; @@ -906,7 +911,7 @@ void GDScript::get_members(HashSet<StringName> *p_members) { } } -const Variant GDScript::get_rpc_config() const { +Variant GDScript::get_rpc_config() const { return rpc_config; } diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 4d21651365..9bb39aac0f 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -334,7 +334,7 @@ public: virtual void get_constants(HashMap<StringName, Variant> *p_constants) override; virtual void get_members(HashSet<StringName> *p_members) override; - virtual const Variant get_rpc_config() const override; + virtual Variant get_rpc_config() const override; void unload_static() const; diff --git a/modules/gdscript/gdscript_lambda_callable.cpp b/modules/gdscript/gdscript_lambda_callable.cpp index 2de5811bca..d6fd5d043b 100644 --- a/modules/gdscript/gdscript_lambda_callable.cpp +++ b/modules/gdscript/gdscript_lambda_callable.cpp @@ -97,25 +97,25 @@ void GDScriptLambdaCallable::call(const Variant **p_arguments, int p_argcount, V } if (captures_amount > 0) { - Vector<const Variant *> args; - args.resize(p_argcount + captures_amount); + const int total_argcount = p_argcount + captures_amount; + const Variant **args = (const Variant **)alloca(sizeof(Variant *) * total_argcount); for (int i = 0; i < captures_amount; i++) { - args.write[i] = &captures[i]; + args[i] = &captures[i]; if (captures[i].get_type() == Variant::OBJECT) { bool was_freed = false; captures[i].get_validated_object_with_check(was_freed); if (was_freed) { ERR_PRINT(vformat(R"(Lambda capture at index %d was freed. Passed "null" instead.)", i)); static Variant nil; - args.write[i] = &nil; + args[i] = &nil; } } } for (int i = 0; i < p_argcount; i++) { - args.write[i + captures_amount] = p_arguments[i]; + args[i + captures_amount] = p_arguments[i]; } - r_return_value = function->call(nullptr, args.ptrw(), args.size(), r_call_error); + r_return_value = function->call(nullptr, args, total_argcount, r_call_error); switch (r_call_error.error) { case Callable::CallError::CALL_ERROR_INVALID_ARGUMENT: r_call_error.argument -= captures_amount; @@ -229,25 +229,25 @@ void GDScriptLambdaSelfCallable::call(const Variant **p_arguments, int p_argcoun } if (captures_amount > 0) { - Vector<const Variant *> args; - args.resize(p_argcount + captures_amount); + const int total_argcount = p_argcount + captures_amount; + const Variant **args = (const Variant **)alloca(sizeof(Variant *) * total_argcount); for (int i = 0; i < captures_amount; i++) { - args.write[i] = &captures[i]; + args[i] = &captures[i]; if (captures[i].get_type() == Variant::OBJECT) { bool was_freed = false; captures[i].get_validated_object_with_check(was_freed); if (was_freed) { ERR_PRINT(vformat(R"(Lambda capture at index %d was freed. Passed "null" instead.)", i)); static Variant nil; - args.write[i] = &nil; + args[i] = &nil; } } } for (int i = 0; i < p_argcount; i++) { - args.write[i + captures_amount] = p_arguments[i]; + args[i + captures_amount] = p_arguments[i]; } - r_return_value = function->call(static_cast<GDScriptInstance *>(object->get_script_instance()), args.ptrw(), args.size(), r_call_error); + r_return_value = function->call(static_cast<GDScriptInstance *>(object->get_script_instance()), args, total_argcount, r_call_error); switch (r_call_error.error) { case Callable::CallError::CALL_ERROR_INVALID_ARGUMENT: r_call_error.argument -= captures_amount; diff --git a/modules/gdscript/language_server/gdscript_text_document.cpp b/modules/gdscript/language_server/gdscript_text_document.cpp index fa5f279db9..06e9775360 100644 --- a/modules/gdscript/language_server/gdscript_text_document.cpp +++ b/modules/gdscript/language_server/gdscript_text_document.cpp @@ -229,19 +229,6 @@ Array GDScriptTextDocument::completion(const Dictionary &p_params) { arr[i] = item.to_json(); i++; } - } else if (GDScriptLanguageProtocol::get_singleton()->is_smart_resolve_enabled()) { - arr = native_member_completions.duplicate(); - - for (KeyValue<String, ExtendGDScriptParser *> &E : GDScriptLanguageProtocol::get_singleton()->get_workspace()->scripts) { - ExtendGDScriptParser *scr = E.value; - const Array &items = scr->get_member_completions(); - - const int start_size = arr.size(); - arr.resize(start_size + items.size()); - for (int i = start_size; i < arr.size(); i++) { - arr[i] = items[i - start_size]; - } - } } return arr; } @@ -485,8 +472,6 @@ GDScriptTextDocument::GDScriptTextDocument() { void GDScriptTextDocument::sync_script_content(const String &p_path, const String &p_content) { String path = GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_path(p_path); GDScriptLanguageProtocol::get_singleton()->get_workspace()->parse_script(path, p_content); - - EditorFileSystem::get_singleton()->update_file(path); } void GDScriptTextDocument::show_native_symbol_in_editor(const String &p_symbol_id) { diff --git a/modules/gdscript/language_server/godot_lsp.h b/modules/gdscript/language_server/godot_lsp.h index bdf339f5fe..6e19cd7a23 100644 --- a/modules/gdscript/language_server/godot_lsp.h +++ b/modules/gdscript/language_server/godot_lsp.h @@ -958,28 +958,30 @@ struct CompletionItem { /** * A string that should be used when comparing this item - * with other items. When `falsy` the label is used. + * with other items. When omitted the label is used + * as the filter text for this item. */ String sortText; /** * A string that should be used when filtering a set of - * completion items. When `falsy` the label is used. + * completion items. When omitted the label is used as the + * filter text for this item. */ String filterText; /** * A string that should be inserted into a document when selecting - * this completion. When `falsy` the label is used. + * this completion. When omitted the label is used as the insert text + * for this item. * * The `insertText` is subject to interpretation by the client side. * Some tools might not take the string literally. For example - * VS Code when code complete is requested in this example `con<cursor position>` - * and a completion item with an `insertText` of `console` is provided it - * will only insert `sole`. Therefore it is recommended to use `textEdit` instead - * since it avoids additional client side interpretation. - * - * @deprecated Use textEdit instead. + * VS Code when code complete is requested in this example + * `con<cursor position>` and a completion item with an `insertText` of + * `console` is provided it will only insert `sole`. Therefore it is + * recommended to use `textEdit` instead since it avoids additional client + * side interpretation. */ String insertText; @@ -1034,14 +1036,20 @@ struct CompletionItem { dict["label"] = label; dict["kind"] = kind; dict["data"] = data; - dict["insertText"] = insertText; + if (!insertText.is_empty()) { + dict["insertText"] = insertText; + } if (resolved) { dict["detail"] = detail; dict["documentation"] = documentation.to_json(); dict["deprecated"] = deprecated; dict["preselect"] = preselect; - dict["sortText"] = sortText; - dict["filterText"] = filterText; + if (!sortText.is_empty()) { + dict["sortText"] = sortText; + } + if (!filterText.is_empty()) { + dict["filterText"] = filterText; + } if (commitCharacters.size()) { dict["commitCharacters"] = commitCharacters; } diff --git a/modules/gdscript/tests/scripts/.editorconfig b/modules/gdscript/tests/scripts/.editorconfig new file mode 100644 index 0000000000..da1efefe3c --- /dev/null +++ b/modules/gdscript/tests/scripts/.editorconfig @@ -0,0 +1,12 @@ +# This file is required to workaround `.editorconfig` autogeneration (see #96845). + +# Some tests handle invalid syntax deliberately; exclude relevant attributes. + +[parser/features/mixed_indentation_on_blank_lines.gd] +trim_trailing_whitespace = false + +[parser/warnings/empty_file_newline.notest.gd] +insert_final_newline = false + +[parser/warnings/empty_file_newline_comment.notest.gd] +insert_final_newline = false diff --git a/modules/gdscript/tests/scripts/parser/.editorconfig b/modules/gdscript/tests/scripts/parser/.editorconfig deleted file mode 100644 index fa43b3ad78..0000000000 --- a/modules/gdscript/tests/scripts/parser/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*.{gd,out}] -trim_trailing_whitespace = false diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_key.gd b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_key.gd new file mode 100644 index 0000000000..2f0b3bd0eb --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_key.gd @@ -0,0 +1,7 @@ +func get_key() -> Variant: + return "key" + +func test(): + var typed: Dictionary[int, int] + typed[get_key()] = 0 + print('not ok') diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_key.out b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_key.out new file mode 100644 index 0000000000..5f6dd7f641 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_key.out @@ -0,0 +1,6 @@ +GDTEST_RUNTIME_ERROR +>> SCRIPT ERROR +>> on function: test() +>> runtime/errors/typed_dictionary_assign_differently_typed_key.gd +>> 6 +>> Invalid assignment of property or key 'key' with value of type 'int' on a base object of type 'Dictionary[int, int]'. diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_value.gd b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_value.gd new file mode 100644 index 0000000000..b171159aed --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_value.gd @@ -0,0 +1,7 @@ +func get_value() -> Variant: + return "value" + +func test(): + var typed: Dictionary[int, int] + typed[0] = get_value() + print("not ok") diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_value.out b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_value.out new file mode 100644 index 0000000000..f766d14261 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed_value.out @@ -0,0 +1,6 @@ +GDTEST_RUNTIME_ERROR +>> SCRIPT ERROR +>> on function: test() +>> runtime/errors/typed_dictionary_assign_differently_typed_value.gd +>> 6 +>> Invalid assignment of property or key '0' with value of type 'String' on a base object of type 'Dictionary[int, int]'. diff --git a/modules/gdscript/tests/scripts/runtime/features/self_destruction.gd b/modules/gdscript/tests/scripts/runtime/features/self_destruction.gd new file mode 100644 index 0000000000..442335faeb --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/self_destruction.gd @@ -0,0 +1,50 @@ +# https://github.com/godotengine/godot/issues/75658 + +class MyObj: + var callable: Callable + + func run(): + callable.call() + + var prop: + set(value): + callable.call() + get: + callable.call() + return 0 + + func _on_some_signal(): + callable.call() + + func _init(p_callable: Callable): + self.callable = p_callable + +signal some_signal + +var obj: MyObj + +func test(): + # Call. + obj = MyObj.new(nullify_obj) + obj.run() + print(obj) + + # Get. + obj = MyObj.new(nullify_obj) + var _aux = obj.prop + print(obj) + + # Set. + obj = MyObj.new(nullify_obj) + obj.prop = 1 + print(obj) + + # Signal handling. + obj = MyObj.new(nullify_obj) + @warning_ignore("return_value_discarded") + some_signal.connect(obj._on_some_signal) + some_signal.emit() + print(obj) + +func nullify_obj(): + obj = null diff --git a/modules/gdscript/tests/scripts/runtime/features/self_destruction.out b/modules/gdscript/tests/scripts/runtime/features/self_destruction.out new file mode 100644 index 0000000000..ee4024a524 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/self_destruction.out @@ -0,0 +1,5 @@ +GDTEST_OK +<null> +<null> +<null> +<null> diff --git a/modules/gltf/doc_classes/GLTFDocumentExtension.xml b/modules/gltf/doc_classes/GLTFDocumentExtension.xml index 5c548c472f..b33e296e1c 100644 --- a/modules/gltf/doc_classes/GLTFDocumentExtension.xml +++ b/modules/gltf/doc_classes/GLTFDocumentExtension.xml @@ -18,7 +18,7 @@ <param index="1" name="gltf_node" type="GLTFNode" /> <param index="2" name="scene_node" type="Node" /> <description> - Part of the export process. This method is run after [method _export_preflight] and before [method _export_preserialize]. + Part of the export process. This method is run after [method _export_preflight] and before [method _export_post_convert]. Runs when converting the data from a Godot scene node. This method can be used to process the Godot scene node data into a format that can be used by [method _export_node]. </description> </method> @@ -41,6 +41,15 @@ This method can be used to modify the final JSON of the generated glTF file. </description> </method> + <method name="_export_post_convert" qualifiers="virtual"> + <return type="int" enum="Error" /> + <param index="0" name="state" type="GLTFState" /> + <param index="1" name="root" type="Node" /> + <description> + Part of the export process. This method is run after [method _convert_scene_node] and before [method _export_preserialize]. + This method can be used to modify the converted node data structures before serialization with any additional data from the scene tree. + </description> + </method> <method name="_export_preflight" qualifiers="virtual"> <return type="int" enum="Error" /> <param index="0" name="state" type="GLTFState" /> @@ -54,7 +63,7 @@ <return type="int" enum="Error" /> <param index="0" name="state" type="GLTFState" /> <description> - Part of the export process. This method is run after [method _convert_scene_node] and before [method _get_saveable_image_formats]. + Part of the export process. This method is run after [method _export_post_convert] and before [method _get_saveable_image_formats]. This method can be used to alter the state before performing serialization. It runs every time when generating a buffer with [method GLTFDocument.generate_buffer] or writing to the file system with [method GLTFDocument.write_to_filesystem]. </description> </method> @@ -64,7 +73,7 @@ <param index="1" name="gltf_node" type="GLTFNode" /> <param index="2" name="scene_parent" type="Node" /> <description> - Part of the import process. This method is run after [method _import_post_parse] and before [method _import_node]. + Part of the import process. This method is run after [method _import_pre_generate] and before [method _import_node]. Runs when generating a Godot scene node from a GLTFNode. The returned node will be added to the scene tree. Multiple nodes can be generated in this step if they are added as a child of the returned node. [b]Note:[/b] The [param scene_parent] parameter may be null if this is the single root node. </description> @@ -113,8 +122,16 @@ <return type="int" enum="Error" /> <param index="0" name="state" type="GLTFState" /> <description> - Part of the import process. This method is run after [method _parse_node_extensions] and before [method _generate_scene_node]. - This method can be used to modify any of the data imported so far after parsing, before generating the nodes and then running the final per-node import step. + Part of the import process. This method is run after [method _parse_node_extensions] and before [method _import_pre_generate]. + This method can be used to modify any of the data imported so far after parsing each node, but before generating the scene or any of its nodes. + </description> + </method> + <method name="_import_pre_generate" qualifiers="virtual"> + <return type="int" enum="Error" /> + <param index="0" name="state" type="GLTFState" /> + <description> + Part of the import process. This method is run after [method _import_post_parse] and before [method _generate_scene_node]. + This method can be used to modify or read from any of the processed data structures, before generating the nodes and then running the final per-node import step. </description> </method> <method name="_import_preflight" qualifiers="virtual"> diff --git a/modules/gltf/doc_classes/GLTFNode.xml b/modules/gltf/doc_classes/GLTFNode.xml index 2786c25e9a..a242a0d1d8 100644 --- a/modules/gltf/doc_classes/GLTFNode.xml +++ b/modules/gltf/doc_classes/GLTFNode.xml @@ -12,6 +12,13 @@ <link title="glTF scene and node spec">https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_004_ScenesNodes.md"</link> </tutorials> <methods> + <method name="append_child_index"> + <return type="void" /> + <param index="0" name="child_index" type="int" /> + <description> + Appends the given child node index to the [member children] array. + </description> + </method> <method name="get_additional_data"> <return type="Variant" /> <param index="0" name="extension_name" type="StringName" /> diff --git a/modules/gltf/doc_classes/GLTFState.xml b/modules/gltf/doc_classes/GLTFState.xml index c049acf557..de7ec2a4ca 100644 --- a/modules/gltf/doc_classes/GLTFState.xml +++ b/modules/gltf/doc_classes/GLTFState.xml @@ -28,6 +28,17 @@ Appends the given byte array data to the buffers and creates a [GLTFBufferView] for it. The index of the destination [GLTFBufferView] is returned. If [param deduplication] is true, the buffers will first be searched for duplicate data, otherwise new bytes will always be appended. </description> </method> + <method name="append_gltf_node"> + <return type="int" /> + <param index="0" name="gltf_node" type="GLTFNode" /> + <param index="1" name="godot_scene_node" type="Node" /> + <param index="2" name="parent_node_index" type="int" /> + <description> + Append the given [GLTFNode] to the state, and return its new index. This can be used to export one Godot node as multiple glTF nodes, or inject new glTF nodes at import time. On import, this must be called before [method GLTFDocumentExtension._generate_scene_node] finishes for the parent node. On export, this must be called before [method GLTFDocumentExtension._export_node] runs for the parent node. + The [param godot_scene_node] parameter is the Godot scene node that corresponds to this glTF node. This is highly recommended to be set to a valid node, but may be null if there is no corresponding Godot scene node. One Godot scene node may be used for multiple glTF nodes, so if exporting multiple glTF nodes for one Godot scene node, use the same Godot scene node for each. + The [param parent_node_index] parameter is the index of the parent [GLTFNode] in the state. If [code]-1[/code], the node will be a root node, otherwise the new node will be added to the parent's list of children. The index will also be written to the [member GLTFNode.parent] property of the new node. + </description> + </method> <method name="get_accessors"> <return type="GLTFAccessor[]" /> <description> diff --git a/modules/gltf/extensions/gltf_document_extension.cpp b/modules/gltf/extensions/gltf_document_extension.cpp index c6540ebb22..6e611762b6 100644 --- a/modules/gltf/extensions/gltf_document_extension.cpp +++ b/modules/gltf/extensions/gltf_document_extension.cpp @@ -38,13 +38,15 @@ void GLTFDocumentExtension::_bind_methods() { GDVIRTUAL_BIND(_parse_image_data, "state", "image_data", "mime_type", "ret_image"); GDVIRTUAL_BIND(_get_image_file_extension); GDVIRTUAL_BIND(_parse_texture_json, "state", "texture_json", "ret_gltf_texture"); - GDVIRTUAL_BIND(_generate_scene_node, "state", "gltf_node", "scene_parent"); GDVIRTUAL_BIND(_import_post_parse, "state"); + GDVIRTUAL_BIND(_import_pre_generate, "state"); + GDVIRTUAL_BIND(_generate_scene_node, "state", "gltf_node", "scene_parent"); GDVIRTUAL_BIND(_import_node, "state", "gltf_node", "json", "node"); GDVIRTUAL_BIND(_import_post, "state", "root"); // Export process. GDVIRTUAL_BIND(_export_preflight, "state", "root"); GDVIRTUAL_BIND(_convert_scene_node, "state", "gltf_node", "scene_node"); + GDVIRTUAL_BIND(_export_post_convert, "state", "root"); GDVIRTUAL_BIND(_export_preserialize, "state"); GDVIRTUAL_BIND(_get_saveable_image_formats); GDVIRTUAL_BIND(_serialize_image_to_bytes, "state", "image", "image_dict", "image_format", "lossy_quality"); @@ -98,6 +100,20 @@ Error GLTFDocumentExtension::parse_texture_json(Ref<GLTFState> p_state, const Di return err; } +Error GLTFDocumentExtension::import_post_parse(Ref<GLTFState> p_state) { + ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); + Error err = OK; + GDVIRTUAL_CALL(_import_post_parse, p_state, err); + return err; +} + +Error GLTFDocumentExtension::import_pre_generate(Ref<GLTFState> p_state) { + ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); + Error err = OK; + GDVIRTUAL_CALL(_import_pre_generate, p_state, err); + return err; +} + Node3D *GLTFDocumentExtension::generate_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_parent) { ERR_FAIL_COND_V(p_state.is_null(), nullptr); ERR_FAIL_COND_V(p_gltf_node.is_null(), nullptr); @@ -106,13 +122,6 @@ Node3D *GLTFDocumentExtension::generate_scene_node(Ref<GLTFState> p_state, Ref<G return ret_node; } -Error GLTFDocumentExtension::import_post_parse(Ref<GLTFState> p_state) { - ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); - Error err = OK; - GDVIRTUAL_CALL(_import_post_parse, p_state, err); - return err; -} - Error GLTFDocumentExtension::import_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &r_dict, Node *p_node) { ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(p_gltf_node.is_null(), ERR_INVALID_PARAMETER); @@ -145,6 +154,14 @@ void GLTFDocumentExtension::convert_scene_node(Ref<GLTFState> p_state, Ref<GLTFN GDVIRTUAL_CALL(_convert_scene_node, p_state, p_gltf_node, p_scene_node); } +Error GLTFDocumentExtension::export_post_convert(Ref<GLTFState> p_state, Node *p_root) { + ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); + ERR_FAIL_NULL_V(p_root, ERR_INVALID_PARAMETER); + Error err = OK; + GDVIRTUAL_CALL(_export_post_convert, p_state, p_root, err); + return err; +} + Error GLTFDocumentExtension::export_preserialize(Ref<GLTFState> p_state) { ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); Error err = OK; diff --git a/modules/gltf/extensions/gltf_document_extension.h b/modules/gltf/extensions/gltf_document_extension.h index 761dff725c..b70710e015 100644 --- a/modules/gltf/extensions/gltf_document_extension.h +++ b/modules/gltf/extensions/gltf_document_extension.h @@ -50,12 +50,14 @@ public: virtual String get_image_file_extension(); virtual Error parse_texture_json(Ref<GLTFState> p_state, const Dictionary &p_texture_json, Ref<GLTFTexture> r_gltf_texture); virtual Error import_post_parse(Ref<GLTFState> p_state); + virtual Error import_pre_generate(Ref<GLTFState> p_state); virtual Node3D *generate_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_parent); virtual Error import_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &r_json, Node *p_node); virtual Error import_post(Ref<GLTFState> p_state, Node *p_node); // Export process. virtual Error export_preflight(Ref<GLTFState> p_state, Node *p_root); virtual void convert_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_node); + virtual Error export_post_convert(Ref<GLTFState> p_state, Node *p_root); virtual Error export_preserialize(Ref<GLTFState> p_state); virtual Vector<String> get_saveable_image_formats(); virtual PackedByteArray serialize_image_to_bytes(Ref<GLTFState> p_state, Ref<Image> p_image, Dictionary p_image_dict, const String &p_image_format, float p_lossy_quality); @@ -71,13 +73,15 @@ public: GDVIRTUAL4R(Error, _parse_image_data, Ref<GLTFState>, PackedByteArray, String, Ref<Image>); GDVIRTUAL0R(String, _get_image_file_extension); GDVIRTUAL3R(Error, _parse_texture_json, Ref<GLTFState>, Dictionary, Ref<GLTFTexture>); - GDVIRTUAL3R(Node3D *, _generate_scene_node, Ref<GLTFState>, Ref<GLTFNode>, Node *); GDVIRTUAL1R(Error, _import_post_parse, Ref<GLTFState>); + GDVIRTUAL1R(Error, _import_pre_generate, Ref<GLTFState>); + GDVIRTUAL3R(Node3D *, _generate_scene_node, Ref<GLTFState>, Ref<GLTFNode>, Node *); GDVIRTUAL4R(Error, _import_node, Ref<GLTFState>, Ref<GLTFNode>, Dictionary, Node *); GDVIRTUAL2R(Error, _import_post, Ref<GLTFState>, Node *); // Export process. GDVIRTUAL2R(Error, _export_preflight, Ref<GLTFState>, Node *); GDVIRTUAL3(_convert_scene_node, Ref<GLTFState>, Ref<GLTFNode>, Node *); + GDVIRTUAL2R(Error, _export_post_convert, Ref<GLTFState>, Node *); GDVIRTUAL1R(Error, _export_preserialize, Ref<GLTFState>); GDVIRTUAL0R(Vector<String>, _get_saveable_image_formats); GDVIRTUAL5R(PackedByteArray, _serialize_image_to_bytes, Ref<GLTFState>, Ref<Image>, Dictionary, String, float); diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index cf1a1ea4b3..992075e980 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -414,7 +414,6 @@ static Vector<real_t> _xform_to_array(const Transform3D p_transform) { Error GLTFDocument::_serialize_nodes(Ref<GLTFState> p_state) { Array nodes; - const int scene_node_count = p_state->scene_nodes.size(); for (int i = 0; i < p_state->nodes.size(); i++) { Dictionary node; Ref<GLTFNode> gltf_node = p_state->nodes[i]; @@ -465,7 +464,7 @@ Error GLTFDocument::_serialize_nodes(Ref<GLTFState> p_state) { } Node *scene_node = nullptr; - if (i < scene_node_count) { + if (i < (int)p_state->scene_nodes.size()) { scene_node = p_state->scene_nodes[i]; } for (Ref<GLTFDocumentExtension> ext : document_extensions) { @@ -5258,6 +5257,7 @@ void GLTFDocument::_convert_scene_node(Ref<GLTFState> p_state, Node *p_current, gltf_node.instantiate(); gltf_node->set_original_name(p_current->get_name()); gltf_node->set_name(_gen_unique_name(p_state, p_current->get_name())); + gltf_node->merge_meta_from(p_current); if (cast_to<Node3D>(p_current)) { Node3D *spatial = cast_to<Node3D>(p_current); _convert_spatial(p_state, spatial, gltf_node); @@ -5303,14 +5303,18 @@ void GLTFDocument::_convert_scene_node(Ref<GLTFState> p_state, Node *p_current, ERR_CONTINUE(ext.is_null()); ext->convert_scene_node(p_state, gltf_node, p_current); } - GLTFNodeIndex current_node_i = p_state->nodes.size(); - GLTFNodeIndex gltf_root = p_gltf_root; - if (gltf_root == -1) { - gltf_root = current_node_i; - p_state->root_nodes.push_back(gltf_root); + GLTFNodeIndex current_node_i; + if (gltf_node->get_parent() == -1) { + current_node_i = p_state->append_gltf_node(gltf_node, p_current, p_gltf_parent); + } else if (gltf_node->get_parent() < -1) { + return; + } else { + current_node_i = p_state->nodes.size() - 1; + while (gltf_node != p_state->nodes[current_node_i]) { + current_node_i--; + } } - gltf_node->merge_meta_from(p_current); - _create_gltf_node(p_state, p_current, current_node_i, p_gltf_parent, gltf_root, gltf_node); + const GLTFNodeIndex gltf_root = (p_gltf_root == -1) ? current_node_i : p_gltf_root; for (int node_i = 0; node_i < p_current->get_child_count(); node_i++) { _convert_scene_node(p_state, p_current->get_child(node_i), current_node_i, gltf_root); } @@ -5371,18 +5375,6 @@ void GLTFDocument::_convert_csg_shape_to_gltf(CSGShape3D *p_current, GLTFNodeInd } #endif // MODULE_CSG_ENABLED -void GLTFDocument::_create_gltf_node(Ref<GLTFState> p_state, Node *p_scene_parent, GLTFNodeIndex p_current_node_i, - GLTFNodeIndex p_parent_node_index, GLTFNodeIndex p_root_gltf_node, Ref<GLTFNode> p_gltf_node) { - p_state->scene_nodes.insert(p_current_node_i, p_scene_parent); - p_state->nodes.push_back(p_gltf_node); - ERR_FAIL_COND(p_current_node_i == p_parent_node_index); - p_state->nodes.write[p_current_node_i]->parent = p_parent_node_index; - if (p_parent_node_index == -1) { - return; - } - p_state->nodes.write[p_parent_node_index]->children.push_back(p_current_node_i); -} - void GLTFDocument::_convert_animation_player_to_gltf(AnimationPlayer *p_animation_player, Ref<GLTFState> p_state, GLTFNodeIndex p_gltf_current, GLTFNodeIndex p_gltf_root_index, Ref<GLTFNode> p_gltf_node, Node *p_scene_parent) { ERR_FAIL_NULL(p_animation_player); p_state->animation_players.push_back(p_animation_player); @@ -5542,6 +5534,10 @@ void GLTFDocument::_convert_skeleton_to_gltf(Skeleton3D *p_skeleton3d, Ref<GLTFS joint_node->set_name(_gen_unique_name(p_state, skeleton->get_bone_name(bone_i))); joint_node->transform = skeleton->get_bone_pose(bone_i); joint_node->joint = true; + + if (p_skeleton3d->has_bone_meta(bone_i, "extras")) { + joint_node->set_meta("extras", p_skeleton3d->get_bone_meta(bone_i, "extras")); + } GLTFNodeIndex current_node_i = p_state->nodes.size(); p_state->scene_nodes.insert(current_node_i, skeleton); p_state->nodes.push_back(joint_node); @@ -7209,6 +7205,12 @@ Node *GLTFDocument::_generate_scene_node_tree(Ref<GLTFState> p_state) { ERR_FAIL_COND_V_MSG(err != OK, nullptr, "glTF: Failed to create skeletons."); err = _create_skins(p_state); ERR_FAIL_COND_V_MSG(err != OK, nullptr, "glTF: Failed to create skins."); + // Run pre-generate for each extension, in case an extension needs to do something before generating the scene. + for (Ref<GLTFDocumentExtension> ext : document_extensions) { + ERR_CONTINUE(ext.is_null()); + err = ext->import_pre_generate(p_state); + ERR_CONTINUE(err != OK); + } // Generate the node tree. Node *single_root; if (p_state->extensions_used.has("GODOT_single_root")) { @@ -7443,6 +7445,12 @@ Error GLTFDocument::append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint state->extensions_used.append("GODOT_single_root"); } _convert_scene_node(state, p_node, -1, -1); + // Run post-convert for each extension, in case an extension needs to do something after converting the scene. + for (Ref<GLTFDocumentExtension> ext : document_extensions) { + ERR_CONTINUE(ext.is_null()); + Error err = ext->export_post_convert(p_state, p_node); + ERR_CONTINUE(err != OK); + } return OK; } diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index b3e6dcf54a..d347d49102 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -342,12 +342,6 @@ public: void _convert_csg_shape_to_gltf(CSGShape3D *p_current, GLTFNodeIndex p_gltf_parent, Ref<GLTFNode> p_gltf_node, Ref<GLTFState> p_state); #endif // MODULE_CSG_ENABLED - void _create_gltf_node(Ref<GLTFState> p_state, - Node *p_scene_parent, - GLTFNodeIndex p_current_node_i, - GLTFNodeIndex p_parent_node_index, - GLTFNodeIndex p_root_gltf_node, - Ref<GLTFNode> p_gltf_node); void _convert_animation_player_to_gltf( AnimationPlayer *p_animation_player, Ref<GLTFState> p_state, GLTFNodeIndex p_gltf_current, diff --git a/modules/gltf/gltf_state.cpp b/modules/gltf/gltf_state.cpp index 73a61ff77f..7763874d02 100644 --- a/modules/gltf/gltf_state.cpp +++ b/modules/gltf/gltf_state.cpp @@ -35,6 +35,7 @@ void GLTFState::_bind_methods() { ClassDB::bind_method(D_METHOD("add_used_extension", "extension_name", "required"), &GLTFState::add_used_extension); ClassDB::bind_method(D_METHOD("append_data_to_buffers", "data", "deduplication"), &GLTFState::append_data_to_buffers); + ClassDB::bind_method(D_METHOD("append_gltf_node", "gltf_node", "godot_scene_node", "parent_node_index"), &GLTFState::append_gltf_node); ClassDB::bind_method(D_METHOD("get_json"), &GLTFState::get_json); ClassDB::bind_method(D_METHOD("set_json", "json"), &GLTFState::set_json); @@ -441,3 +442,16 @@ GLTFBufferViewIndex GLTFState::append_data_to_buffers(const Vector<uint8_t> &p_d buffer_views.push_back(buffer_view); return new_index; } + +GLTFNodeIndex GLTFState::append_gltf_node(Ref<GLTFNode> p_gltf_node, Node *p_godot_scene_node, GLTFNodeIndex p_parent_node_index) { + p_gltf_node->set_parent(p_parent_node_index); + const GLTFNodeIndex new_index = nodes.size(); + nodes.append(p_gltf_node); + scene_nodes.insert(new_index, p_godot_scene_node); + if (p_parent_node_index == -1) { + root_nodes.append(new_index); + } else if (p_parent_node_index < new_index) { + nodes.write[p_parent_node_index]->append_child_index(new_index); + } + return new_index; +} diff --git a/modules/gltf/gltf_state.h b/modules/gltf/gltf_state.h index 07efafe13b..7954049192 100644 --- a/modules/gltf/gltf_state.h +++ b/modules/gltf/gltf_state.h @@ -119,6 +119,7 @@ public: void add_used_extension(const String &p_extension, bool p_required = false); GLTFBufferViewIndex append_data_to_buffers(const Vector<uint8_t> &p_data, const bool p_deduplication); + GLTFNodeIndex append_gltf_node(Ref<GLTFNode> p_gltf_node, Node *p_godot_scene_node, GLTFNodeIndex p_parent_node_index); enum GLTFHandleBinary { HANDLE_BINARY_DISCARD_TEXTURES = 0, diff --git a/modules/gltf/skin_tool.cpp b/modules/gltf/skin_tool.cpp index a344334d93..1522c0e324 100644 --- a/modules/gltf/skin_tool.cpp +++ b/modules/gltf/skin_tool.cpp @@ -602,6 +602,11 @@ Error SkinTool::_create_skeletons( skeleton->set_bone_pose_rotation(bone_index, node->transform.basis.get_rotation_quaternion()); skeleton->set_bone_pose_scale(bone_index, node->transform.basis.get_scale()); + // Store bone-level GLTF extras in skeleton per bone meta. + if (node->has_meta("extras")) { + skeleton->set_bone_meta(bone_index, "extras", node->get_meta("extras")); + } + if (node->parent >= 0 && nodes[node->parent]->skeleton == skel_i) { const int bone_parent = skeleton->find_bone(nodes[node->parent]->get_name()); ERR_FAIL_COND_V(bone_parent < 0, FAILED); diff --git a/modules/gltf/structures/gltf_node.cpp b/modules/gltf/structures/gltf_node.cpp index 2934e4b5ee..ccee5e8ca4 100644 --- a/modules/gltf/structures/gltf_node.cpp +++ b/modules/gltf/structures/gltf_node.cpp @@ -55,6 +55,7 @@ void GLTFNode::_bind_methods() { ClassDB::bind_method(D_METHOD("set_scale", "scale"), &GLTFNode::set_scale); ClassDB::bind_method(D_METHOD("get_children"), &GLTFNode::get_children); ClassDB::bind_method(D_METHOD("set_children", "children"), &GLTFNode::set_children); + ClassDB::bind_method(D_METHOD("append_child_index", "child_index"), &GLTFNode::append_child_index); ClassDB::bind_method(D_METHOD("get_light"), &GLTFNode::get_light); ClassDB::bind_method(D_METHOD("set_light", "light"), &GLTFNode::set_light); ClassDB::bind_method(D_METHOD("get_additional_data", "extension_name"), &GLTFNode::get_additional_data); @@ -170,6 +171,10 @@ void GLTFNode::set_children(Vector<int> p_children) { children = p_children; } +void GLTFNode::append_child_index(int p_child_index) { + children.append(p_child_index); +} + GLTFLightIndex GLTFNode::get_light() { return light; } diff --git a/modules/gltf/structures/gltf_node.h b/modules/gltf/structures/gltf_node.h index 63399fb32b..f3f6bfa2f1 100644 --- a/modules/gltf/structures/gltf_node.h +++ b/modules/gltf/structures/gltf_node.h @@ -97,6 +97,7 @@ public: Vector<int> get_children(); void set_children(Vector<int> p_children); + void append_child_index(int p_child_index); GLTFLightIndex get_light(); void set_light(GLTFLightIndex p_light); diff --git a/modules/gltf/tests/test_gltf_extras.h b/modules/gltf/tests/test_gltf_extras.h index 96aadf3023..37c8f6925c 100644 --- a/modules/gltf/tests/test_gltf_extras.h +++ b/modules/gltf/tests/test_gltf_extras.h @@ -41,6 +41,7 @@ #include "modules/gltf/gltf_document.h" #include "modules/gltf/gltf_state.h" #include "scene/3d/mesh_instance_3d.h" +#include "scene/3d/skeleton_3d.h" #include "scene/main/window.h" #include "scene/resources/3d/primitive_meshes.h" #include "scene/resources/material.h" @@ -158,6 +159,62 @@ TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import" memdelete(original); memdelete(loaded); } + +TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") { + // Setup scene. + Skeleton3D *skeleton = memnew(Skeleton3D); + skeleton->set_name("skeleton"); + Dictionary skeleton_extras; + skeleton_extras["node_type"] = "skeleton"; + skeleton->set_meta("extras", skeleton_extras); + + skeleton->add_bone("parent"); + skeleton->set_bone_rest(0, Transform3D()); + Dictionary parent_bone_extras; + parent_bone_extras["bone"] = "i_am_parent_bone"; + skeleton->set_bone_meta(0, "extras", parent_bone_extras); + + skeleton->add_bone("child"); + skeleton->set_bone_rest(1, Transform3D()); + skeleton->set_bone_parent(1, 0); + Dictionary child_bone_extras; + child_bone_extras["bone"] = "i_am_child_bone"; + skeleton->set_bone_meta(1, "extras", child_bone_extras); + + // We have to have a mesh to link with skeleton or it will not get imported. + Ref<PlaneMesh> meshdata = memnew(PlaneMesh); + meshdata->set_name("planemesh"); + + MeshInstance3D *mesh = memnew(MeshInstance3D); + mesh->set_mesh(meshdata); + mesh->set_name("mesh_instance_3d"); + + Node3D *scene = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(scene); + scene->add_child(skeleton); + scene->add_child(mesh); + scene->set_name("node3d"); + + // Now that both skeleton and mesh are part of scene, link them. + mesh->set_skeleton_path(mesh->get_path_to(skeleton)); + + // Convert to GLFT and back. + String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_bone_extras"); + Node *loaded = _gltf_export_then_import(scene, tempfile); + + // Compare the results. + CHECK(loaded->get_name() == "node3d"); + Skeleton3D *result = Object::cast_to<Skeleton3D>(loaded->find_child("Skeleton3D", false, true)); + CHECK(result->get_bone_name(0) == "parent"); + CHECK(Dictionary(result->get_bone_meta(0, "extras"))["bone"] == "i_am_parent_bone"); + CHECK(result->get_bone_name(1) == "child"); + CHECK(Dictionary(result->get_bone_meta(1, "extras"))["bone"] == "i_am_child_bone"); + + memdelete(skeleton); + memdelete(mesh); + memdelete(scene); + memdelete(loaded); +} } // namespace TestGltfExtras #endif // TOOLS_ENABLED diff --git a/modules/gridmap/editor/grid_map_editor_plugin.cpp b/modules/gridmap/editor/grid_map_editor_plugin.cpp index 1c658b9e9b..4c11565c51 100644 --- a/modules/gridmap/editor/grid_map_editor_plugin.cpp +++ b/modules/gridmap/editor/grid_map_editor_plugin.cpp @@ -816,13 +816,14 @@ void GridMapEditor::_text_changed(const String &p_text) { update_palette(); } -void GridMapEditor::_sbox_input(const Ref<InputEvent> &p_ie) { - const Ref<InputEventKey> k = p_ie; - - if (k.is_valid() && (k->get_keycode() == Key::UP || k->get_keycode() == Key::DOWN || k->get_keycode() == Key::PAGEUP || k->get_keycode() == Key::PAGEDOWN)) { - // Forward the key input to the ItemList so it can be scrolled - mesh_library_palette->gui_input(k); - search_box->accept_event(); +void GridMapEditor::_sbox_input(const Ref<InputEvent> &p_event) { + // Redirect navigational key events to the item list. + Ref<InputEventKey> key = p_event; + if (key.is_valid()) { + if (key->is_action("ui_up", true) || key->is_action("ui_down", true) || key->is_action("ui_page_up") || key->is_action("ui_page_down")) { + mesh_library_palette->gui_input(key); + search_box->accept_event(); + } } } diff --git a/modules/gridmap/editor/grid_map_editor_plugin.h b/modules/gridmap/editor/grid_map_editor_plugin.h index cfa0f0c35c..4294c93c93 100644 --- a/modules/gridmap/editor/grid_map_editor_plugin.h +++ b/modules/gridmap/editor/grid_map_editor_plugin.h @@ -199,7 +199,7 @@ class GridMapEditor : public VBoxContainer { void _update_theme(); void _text_changed(const String &p_text); - void _sbox_input(const Ref<InputEvent> &p_ie); + void _sbox_input(const Ref<InputEvent> &p_event); void _mesh_library_palette_input(const Ref<InputEvent> &p_ie); void _icon_size_changed(float p_value); diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 5d59c33636..f47e6d209a 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -2717,7 +2717,7 @@ int CSharpScript::get_member_line(const StringName &p_member) const { return -1; } -const Variant CSharpScript::get_rpc_config() const { +Variant CSharpScript::get_rpc_config() const { return rpc_config; } diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index c48e1a95c9..ec7328be4a 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -284,7 +284,7 @@ public: int get_member_line(const StringName &p_member) const override; - const Variant get_rpc_config() const override; + Variant get_rpc_config() const override; #ifdef TOOLS_ENABLED bool is_placeholder_fallback_enabled() const override { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj index ee624a443d..c5f2dfee4b 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj @@ -30,6 +30,7 @@ <None Include="$(GodotSdkPackageVersionsFilePath)" Pack="true" PackagePath="Sdk"> <Link>Sdk\SdkPackageVersions.props</Link> </None> + <None Include="Sdk\Android.props" Pack="true" PackagePath="Sdk" /> <None Include="Sdk\iOSNativeAOT.props" Pack="true" PackagePath="Sdk" /> <None Include="Sdk\iOSNativeAOT.targets" Pack="true" PackagePath="Sdk" /> </ItemGroup> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Android.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Android.props new file mode 100644 index 0000000000..3926a4b22a --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Android.props @@ -0,0 +1,5 @@ +<Project> + <PropertyGroup> + <UseMonoRuntime Condition=" '$(UseMonoRuntime)' == '' and '$(PublishAot)' != 'true' ">true</UseMonoRuntime> + </PropertyGroup> +</Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props index c4034f1f9f..d10f9ae0ab 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props @@ -112,5 +112,6 @@ <DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants> </PropertyGroup> + <Import Project="$(MSBuildThisFileDirectory)\Android.props" Condition=" '$(GodotTargetPlatform)' == 'android' " /> <Import Project="$(MSBuildThisFileDirectory)\iOSNativeAOT.props" Condition=" '$(GodotTargetPlatform)' == 'ios' " /> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index a5f24fb67b..6fd84d3834 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -245,7 +245,6 @@ namespace GodotTools.Export { publishOutputDir = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, "godot-publish-dotnet", $"{buildConfig}-{runtimeIdentifier}"); - } outputPaths.Add(publishOutputDir); @@ -322,6 +321,30 @@ namespace GodotTools.Export { if (embedBuildResults) { + if (platform == OS.Platforms.Android) + { + if (IsSharedObject(Path.GetFileName(path))) + { + AddSharedObject(path, tags: new string[] { arch }, + Path.Join(projectDataDirName, + Path.GetRelativePath(publishOutputDir, + Path.GetDirectoryName(path)!))); + + return; + } + + static bool IsSharedObject(string fileName) + { + if (fileName.EndsWith(".so") || fileName.EndsWith(".a") + || fileName.EndsWith(".jar") || fileName.EndsWith(".dex")) + { + return true; + } + + return false; + } + } + string filePath = SanitizeSlashes(Path.GetRelativePath(publishOutputDir, path)); byte[] fileData = File.ReadAllBytes(path); string hash = Convert.ToBase64String(SHA512.HashData(fileData)); diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index 48caae8523..6abf0193cf 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/modules/mono/mono_gd/gd_mono.cpp @@ -61,6 +61,14 @@ hostfxr_initialize_for_runtime_config_fn hostfxr_initialize_for_runtime_config = hostfxr_get_runtime_delegate_fn hostfxr_get_runtime_delegate = nullptr; hostfxr_close_fn hostfxr_close = nullptr; +#ifndef TOOLS_ENABLED +typedef int(CORECLR_DELEGATE_CALLTYPE *coreclr_create_delegate_fn)(void *hostHandle, unsigned int domainId, const char *entryPointAssemblyName, const char *entryPointTypeName, const char *entryPointMethodName, void **delegate); +typedef int(CORECLR_DELEGATE_CALLTYPE *coreclr_initialize_fn)(const char *exePath, const char *appDomainFriendlyName, int propertyCount, const char **propertyKeys, const char **propertyValues, void **hostHandle, unsigned int *domainId); + +coreclr_create_delegate_fn coreclr_create_delegate = nullptr; +coreclr_initialize_fn coreclr_initialize = nullptr; +#endif + #ifdef _WIN32 static_assert(sizeof(char_t) == sizeof(char16_t)); using HostFxrCharString = Char16String; @@ -142,6 +150,56 @@ String find_hostfxr() { #endif } +#ifndef TOOLS_ENABLED +String find_monosgen() { +#if defined(ANDROID_ENABLED) + // Android includes all native libraries in the libs directory of the APK + // so we assume it exists and use only the name to dlopen it. + return "libmonosgen-2.0.so"; +#else +#if defined(WINDOWS_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("monosgen-2.0.dll"); +#elif defined(MACOS_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("libmonosgen-2.0.dylib"); +#elif defined(UNIX_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("libmonosgen-2.0.so"); +#else +#error "Platform not supported (yet?)" +#endif + + if (FileAccess::exists(probe_path)) { + return probe_path; + } + + return String(); +#endif +} + +String find_coreclr() { +#if defined(WINDOWS_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("coreclr.dll"); +#elif defined(MACOS_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("libcoreclr.dylib"); +#elif defined(UNIX_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("libcoreclr.so"); +#else +#error "Platform not supported (yet?)" +#endif + + if (FileAccess::exists(probe_path)) { + return probe_path; + } + + return String(); +} +#endif + bool load_hostfxr(void *&r_hostfxr_dll_handle) { String hostfxr_path = find_hostfxr(); @@ -182,6 +240,47 @@ bool load_hostfxr(void *&r_hostfxr_dll_handle) { hostfxr_close); } +#ifndef TOOLS_ENABLED +bool load_coreclr(void *&r_coreclr_dll_handle) { + String coreclr_path = find_coreclr(); + + bool is_monovm = false; + if (coreclr_path.is_empty()) { + // Fallback to MonoVM (should have the same API as CoreCLR). + coreclr_path = find_monosgen(); + is_monovm = true; + } + + if (coreclr_path.is_empty()) { + return false; + } + + const String coreclr_name = is_monovm ? "monosgen" : "coreclr"; + print_verbose("Found " + coreclr_name + ": " + coreclr_path); + + Error err = OS::get_singleton()->open_dynamic_library(coreclr_path, r_coreclr_dll_handle); + + if (err != OK) { + return false; + } + + void *lib = r_coreclr_dll_handle; + + void *symbol = nullptr; + + err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "coreclr_initialize", symbol); + ERR_FAIL_COND_V(err != OK, false); + coreclr_initialize = (coreclr_initialize_fn)symbol; + + err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "coreclr_create_delegate", symbol); + ERR_FAIL_COND_V(err != OK, false); + coreclr_create_delegate = (coreclr_create_delegate_fn)symbol; + + return (coreclr_initialize && + coreclr_create_delegate); +} +#endif + #ifdef TOOLS_ENABLED load_assembly_and_get_function_pointer_fn initialize_hostfxr_for_config(const char_t *p_config_path) { hostfxr_handle cxt = nullptr; @@ -339,6 +438,56 @@ godot_plugins_initialize_fn try_load_native_aot_library(void *&r_aot_dll_handle) } #endif +#ifndef TOOLS_ENABLED +String make_tpa_list() { + String tpa_list; + +#if defined(WINDOWS_ENABLED) + String separator = ";"; +#else + String separator = ":"; +#endif + + String assemblies_dir = GodotSharpDirs::get_api_assemblies_dir(); + PackedStringArray files = DirAccess::get_files_at(assemblies_dir); + for (const String &file : files) { + tpa_list += assemblies_dir.path_join(file); + tpa_list += separator; + } + + return tpa_list; +} + +godot_plugins_initialize_fn initialize_coreclr_and_godot_plugins(bool &r_runtime_initialized) { + godot_plugins_initialize_fn godot_plugins_initialize = nullptr; + + String assembly_name = path::get_csharp_project_name(); + + String tpa_list = make_tpa_list(); + const char *prop_keys[] = { "TRUSTED_PLATFORM_ASSEMBLIES" }; + const char *prop_values[] = { tpa_list.utf8().get_data() }; + int nprops = sizeof(prop_keys) / sizeof(prop_keys[0]); + + void *coreclr_handle = nullptr; + unsigned int domain_id = 0; + int rc = coreclr_initialize(nullptr, nullptr, nprops, (const char **)&prop_keys, (const char **)&prop_values, &coreclr_handle, &domain_id); + ERR_FAIL_COND_V_MSG(rc != 0, nullptr, ".NET: Failed to initialize CoreCLR."); + + r_runtime_initialized = true; + + print_verbose(".NET: CoreCLR initialized"); + + coreclr_create_delegate(coreclr_handle, domain_id, + assembly_name.utf8().get_data(), + "GodotPlugins.Game.Main", + "InitializeFromGameProject", + (void **)&godot_plugins_initialize); + ERR_FAIL_NULL_V_MSG(godot_plugins_initialize, nullptr, ".NET: Failed to get GodotPlugins initialization function pointer"); + + return godot_plugins_initialize; +} +#endif + } // namespace bool GDMono::should_initialize() { @@ -382,14 +531,21 @@ void GDMono::initialize() { } #endif - if (!load_hostfxr(hostfxr_dll_handle)) { + if (load_hostfxr(hostfxr_dll_handle)) { + godot_plugins_initialize = initialize_hostfxr_and_godot_plugins(runtime_initialized); + ERR_FAIL_NULL(godot_plugins_initialize); + } else { #if !defined(TOOLS_ENABLED) - godot_plugins_initialize = try_load_native_aot_library(hostfxr_dll_handle); - - if (godot_plugins_initialize != nullptr) { - is_native_aot = true; - runtime_initialized = true; + if (load_coreclr(coreclr_dll_handle)) { + godot_plugins_initialize = initialize_coreclr_and_godot_plugins(runtime_initialized); } else { + godot_plugins_initialize = try_load_native_aot_library(hostfxr_dll_handle); + if (godot_plugins_initialize != nullptr) { + runtime_initialized = true; + } + } + + if (godot_plugins_initialize == nullptr) { ERR_FAIL_MSG(".NET: Failed to load hostfxr"); } #else @@ -400,11 +556,6 @@ void GDMono::initialize() { #endif } - if (!is_native_aot) { - godot_plugins_initialize = initialize_hostfxr_and_godot_plugins(runtime_initialized); - ERR_FAIL_NULL(godot_plugins_initialize); - } - int32_t interop_funcs_size = 0; const void **interop_funcs = godotsharp::get_runtime_interop_funcs(interop_funcs_size); @@ -553,6 +704,9 @@ GDMono::~GDMono() { if (hostfxr_dll_handle) { OS::get_singleton()->close_dynamic_library(hostfxr_dll_handle); } + if (coreclr_dll_handle) { + OS::get_singleton()->close_dynamic_library(coreclr_dll_handle); + } finalizing_scripts_domain = false; runtime_initialized = false; diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h index 614bfc63fb..fae3421ac9 100644 --- a/modules/mono/mono_gd/gd_mono.h +++ b/modules/mono/mono_gd/gd_mono.h @@ -64,7 +64,7 @@ class GDMono { bool finalizing_scripts_domain = false; void *hostfxr_dll_handle = nullptr; - bool is_native_aot = false; + void *coreclr_dll_handle = nullptr; String project_assembly_path; uint64_t project_assembly_modified_time = 0; diff --git a/modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar b/modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar Binary files differnew file mode 100755 index 0000000000..7366030881 --- /dev/null +++ b/modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar diff --git a/modules/multiplayer/doc_classes/SceneMultiplayer.xml b/modules/multiplayer/doc_classes/SceneMultiplayer.xml index 7abee8e2c8..42f32d4848 100644 --- a/modules/multiplayer/doc_classes/SceneMultiplayer.xml +++ b/modules/multiplayer/doc_classes/SceneMultiplayer.xml @@ -68,7 +68,7 @@ The callback to execute when when receiving authentication data sent via [method send_auth]. If the [Callable] is empty (default), peers will be automatically accepted as soon as they connect. </member> <member name="auth_timeout" type="float" setter="set_auth_timeout" getter="get_auth_timeout" default="3.0"> - If set to a value greater than [code]0.0[/code], the maximum amount of time peers can stay in the authenticating state, after which the authentication will automatically fail. See the [signal peer_authenticating] and [signal peer_authentication_failed] signals. + If set to a value greater than [code]0.0[/code], the maximum duration in seconds peers can stay in the authenticating state, after which the authentication will automatically fail. See the [signal peer_authenticating] and [signal peer_authentication_failed] signals. </member> <member name="max_delta_packet_size" type="int" setter="set_max_delta_packet_size" getter="get_max_delta_packet_size" default="65535"> Maximum size of each delta packet. Higher values increase the chance of receiving full updates in a single frame, but also the chance of causing networking congestion (higher latency, disconnections). See [MultiplayerSynchronizer]. diff --git a/modules/multiplayer/editor/replication_editor.cpp b/modules/multiplayer/editor/replication_editor.cpp index 851ad85876..386feae4f9 100644 --- a/modules/multiplayer/editor/replication_editor.cpp +++ b/modules/multiplayer/editor/replication_editor.cpp @@ -93,24 +93,6 @@ void ReplicationEditor::_pick_node_select_recursive(TreeItem *p_item, const Stri } } -void ReplicationEditor::_pick_node_filter_input(const Ref<InputEvent> &p_ie) { - Ref<InputEventKey> k = p_ie; - - if (k.is_valid()) { - switch (k->get_keycode()) { - case Key::UP: - case Key::DOWN: - case Key::PAGEUP: - case Key::PAGEDOWN: { - pick_node->get_scene_tree()->get_scene_tree()->gui_input(k); - pick_node->get_filter_line_edit()->accept_event(); - } break; - default: - break; - } - } -} - void ReplicationEditor::_pick_node_selected(NodePath p_path) { Node *root = current->get_node(current->get_root_path()); ERR_FAIL_NULL(root); @@ -184,11 +166,9 @@ ReplicationEditor::ReplicationEditor() { pick_node = memnew(SceneTreeDialog); add_child(pick_node); - pick_node->register_text_enter(pick_node->get_filter_line_edit()); pick_node->set_title(TTR("Pick a node to synchronize:")); pick_node->connect("selected", callable_mp(this, &ReplicationEditor::_pick_node_selected)); pick_node->get_filter_line_edit()->connect(SceneStringName(text_changed), callable_mp(this, &ReplicationEditor::_pick_node_filter_text_changed)); - pick_node->get_filter_line_edit()->connect("gui_input", callable_mp(this, &ReplicationEditor::_pick_node_filter_input)); prop_selector = memnew(PropertySelector); add_child(prop_selector); diff --git a/modules/multiplayer/editor/replication_editor.h b/modules/multiplayer/editor/replication_editor.h index 8f11774292..017fa73967 100644 --- a/modules/multiplayer/editor/replication_editor.h +++ b/modules/multiplayer/editor/replication_editor.h @@ -81,7 +81,6 @@ private: void _pick_node_filter_text_changed(const String &p_newtext); void _pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector<Node *> &p_select_candidates); - void _pick_node_filter_input(const Ref<InputEvent> &p_ie); void _pick_node_selected(NodePath p_path); void _pick_new_property(); diff --git a/modules/multiplayer/scene_rpc_interface.cpp b/modules/multiplayer/scene_rpc_interface.cpp index 69bb19c01c..0938d7ef99 100644 --- a/modules/multiplayer/scene_rpc_interface.cpp +++ b/modules/multiplayer/scene_rpc_interface.cpp @@ -118,7 +118,7 @@ const SceneRPCInterface::RPCConfigCache &SceneRPCInterface::_get_node_config(con return rpc_cache[oid]; } RPCConfigCache cache; - _parse_rpc_config(p_node->get_node_rpc_config(), true, cache); + _parse_rpc_config(p_node->get_rpc_config(), true, cache); if (p_node->get_script_instance()) { _parse_rpc_config(p_node->get_script_instance()->get_rpc_config(), false, cache); } |