From 56de6921cd47d271fee891f1061b4d5ba115a310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ptaszek?= Date: Tue, 1 Sep 2026 11:57:01 +0200 Subject: [PATCH] Add gl.CopyImageSubData (#3299) Expose glCopyImageSubData (ARB_copy_image) to Lua. Until now the only way to fill a layer of a GL.TEXTURE_2D_ARRAY, or to combine two textures, was a render-to-texture pass, which forces an uncompressed RGBA8 copy even when the source is a compressed texture and costs a fullscreen draw per layer. A direct image copy keeps the source format (compressed to compressed included) and needs no FBO. The destination must be a Lua-created texture, the source can be any texture name gl.Texture accepts. Offsets and sizes are validated against the known texture dimensions. The function is only registered when the driver supports ARB_copy_image. --- rts/Lua/LuaOpenGL.cpp | 129 ++++++++++++++++++++++++++++++++++++++++++ rts/Lua/LuaOpenGL.h | 1 + 2 files changed, 130 insertions(+) diff --git a/rts/Lua/LuaOpenGL.cpp b/rts/Lua/LuaOpenGL.cpp index 131db1c09b3..81aa67ea982 100644 --- a/rts/Lua/LuaOpenGL.cpp +++ b/rts/Lua/LuaOpenGL.cpp @@ -10,6 +10,8 @@ #include "Rendering/GL/myGL.h" +#include +#include #include #include #include @@ -350,6 +352,8 @@ bool LuaOpenGL::PushEntries(lua_State* L) REGISTER_LUA_CFUNC(DeleteTexture); REGISTER_LUA_CFUNC(TextureInfo); REGISTER_LUA_CFUNC(CopyToTexture); + if (GLAD_GL_ARB_copy_image) + REGISTER_LUA_CFUNC(CopyImageSubData); if (FBO::IsSupported()) { // FIXME: obsolete REGISTER_LUA_CFUNC(DeleteTextureFBO); @@ -4376,6 +4380,131 @@ int LuaOpenGL::CopyToTexture(lua_State* L) } +/*** + * @function gl.CopyImageSubData + * @param srcName string + * @param srcLevel integer + * @param srcX integer + * @param srcY integer + * @param srcZ integer + * @param dstName string + * @param dstLevel integer + * @param dstX integer + * @param dstY integer + * @param dstZ integer + * @param width integer + * @param height integer + * @param depth integer + */ +int LuaOpenGL::CopyImageSubData(lua_State* L) +{ + CheckDrawingEnabled(L, __func__); + + const auto CheckInteger = [L](int index) { + const lua_Number value = luaL_checknumber(L, index); + + if (!std::isfinite(value) || value != std::trunc(value) || value < std::numeric_limits::min() || value > std::numeric_limits::max()) + luaL_argerror(L, index, "integer out of range"); + + return static_cast(value); + }; + + LuaMatTexture srcTex; + + if (!LuaOpenGLUtils::ParseTextureImage(L, srcTex, luaL_checkstring(L, 1))) + luaL_error(L, "gl.CopyImageSubData() invalid source texture"); + + // ParseTextureImage accepts unresolved named textures. + if (srcTex.GetTextureID() == 0) + luaL_error(L, "gl.CopyImageSubData() source texture does not exist"); + + const std::string& dstName = luaL_checkstring(L, 6); + + if (dstName[0] != LuaTextures::prefix) // '!' + luaL_error(L, "gl.CopyImageSubData() can only write to lua textures"); + + const LuaTextures& textures = CLuaHandle::GetActiveTextures(L); + const LuaTextures::Texture* dstTex = textures.GetInfo(dstName); + + if (dstTex == nullptr) + luaL_error(L, "gl.CopyImageSubData() unknown destination texture"); + + const GLint srcLevel = CheckInteger(2); + const GLint srcX = CheckInteger(3); + const GLint srcY = CheckInteger(4); + const GLint srcZ = CheckInteger(5); + const GLint dstLevel = CheckInteger(7); + const GLint dstX = CheckInteger(8); + const GLint dstY = CheckInteger(9); + const GLint dstZ = CheckInteger(10); + const GLsizei width = CheckInteger(11); + const GLsizei height = CheckInteger(12); + const GLsizei depth = CheckInteger(13); + + if (srcLevel < 0 || dstLevel < 0 || srcX < 0 || srcY < 0 || srcZ < 0 || dstX < 0 || dstY < 0 || dstZ < 0 || width <= 0 || height <= 0 || depth <= 0) + luaL_error(L, "gl.CopyImageSubData() negative offset or non-positive size"); + + const auto LevelSize = [](int size, GLint level) { + return (level < std::numeric_limits::digits)? std::max(1, size >> level): 1; + }; + const auto ImageSize = [&LevelSize](GLenum target, int x, int y, int z, GLint level) { + switch (target) { + case GL_TEXTURE_1D: return std::tuple(LevelSize(x, level), 1, 1); + case GL_TEXTURE_2D: return std::tuple(LevelSize(x, level), LevelSize(y, level), 1); + case GL_TEXTURE_2D_ARRAY: return std::tuple(LevelSize(x, level), LevelSize(y, level), z); + case GL_TEXTURE_3D: return std::tuple(LevelSize(x, level), LevelSize(y, level), LevelSize(z, level)); + case GL_TEXTURE_CUBE_MAP: return std::tuple(LevelSize(x, level), LevelSize(y, level), 6); + case GL_TEXTURE_2D_MULTISAMPLE: return std::tuple(x, y, 1); + default: return std::tuple(0, 0, 0); + } + }; + const auto RegionFits = [](GLint offset, GLsizei extent, int size) { + return (size <= 0 || (offset <= size && extent <= size - offset)); + }; + const auto MaxMipLevel = [](GLenum target, int x, int y, int z) { + int size = x; + + if (target != GL_TEXTURE_1D) + size = std::max(size, y); + if (target == GL_TEXTURE_3D) + size = std::max(size, z); + + GLint level = 0; + while (size > 1) { + size >>= 1; + ++level; + } + + return level; + }; + + const auto [srcXSize, srcYSize, srcZSize] = srcTex.GetSize(); + const GLenum srcTarget = srcTex.GetTextureTarget(); + const auto [srcLevelX, srcLevelY, srcLevelZ] = ImageSize(srcTarget, srcXSize, srcYSize, srcZSize, srcLevel); + const auto [dstLevelX, dstLevelY, dstLevelZ] = ImageSize(dstTex->target, dstTex->xsize, dstTex->ysize, dstTex->zsize, dstLevel); + + if ((srcXSize > 0 && srcLevel > MaxMipLevel(srcTarget, srcXSize, srcYSize, srcZSize)) || dstLevel > MaxMipLevel(dstTex->target, dstTex->xsize, dstTex->ysize, dstTex->zsize)) + luaL_error(L, "gl.CopyImageSubData() mip level out of bounds"); + + if ((srcTarget == GL_TEXTURE_2D_MULTISAMPLE && srcLevel != 0) || (dstTex->target == GL_TEXTURE_2D_MULTISAMPLE && dstLevel != 0)) + luaL_error(L, "gl.CopyImageSubData() invalid multisample mip level"); + + if (!RegionFits(srcX, width, srcLevelX) || !RegionFits(srcY, height, srcLevelY) || !RegionFits(srcZ, depth, srcLevelZ)) + luaL_error(L, "gl.CopyImageSubData() source region out of bounds"); + + if (!RegionFits(dstX, width, dstLevelX) || !RegionFits(dstY, height, dstLevelY) || !RegionFits(dstZ, depth, dstLevelZ)) + luaL_error(L, "gl.CopyImageSubData() destination region out of bounds"); + + glCopyImageSubData( + srcTex.GetTextureID(), srcTarget, srcLevel, srcX, srcY, srcZ, + dstTex->id, dstTex->target, dstLevel, dstX, dstY, dstZ, + width, height, depth + ); + + return 0; +} + + // FIXME: obsolete /*** * @function gl.RenderToTexture diff --git a/rts/Lua/LuaOpenGL.h b/rts/Lua/LuaOpenGL.h index fbe3597eafa..827fa54c68e 100644 --- a/rts/Lua/LuaOpenGL.h +++ b/rts/Lua/LuaOpenGL.h @@ -231,6 +231,7 @@ class LuaOpenGL { static int DeleteTextureFBO(lua_State* L); static int TextureInfo(lua_State* L); static int CopyToTexture(lua_State* L); + static int CopyImageSubData(lua_State* L); static int RenderToTexture(lua_State* L); static int GenerateMipmap(lua_State* L); static int ActiveTexture(lua_State* L);