# The contents of this file are subject to the Common Public Attribution License # Version 1.0 (the “License”); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla # Public License Version 1.1 but Sections 14 and 15 have been added to cover use # of software over a computer network and provide for limited attribution for # the Original Developer. In addition, Exhibit A has been modified to be # consistent with Exhibit B. # # Software distributed under the License is distributed on an “AS IS” basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. # # The Original Code is Myou Engine. # # the Original Developer is the Initial Developer. # # The Initial Developer of the Original Code is the Myou Engine developers. # All portions of the code written by the Myou Engine developers are Copyright # (c) 2024. All Rights Reserved. # # Alternatively, the contents of this file may be used under the terms of the # GNU Affero General Public License version 3 (the [AGPL-3] License), in which # case the provisions of [AGPL-3] License are applicable instead of those above. # # If you wish to allow use of your version of this file only under the terms of # the [AGPL-3] License and not to allow others to use your version of this file # under the CPAL, indicate your decision by deleting the provisions above and # replace them with the notice and other provisions required by the [AGPL-3] # License. If you do not delete the provisions above, a recipient may use your # version of this file under either the CPAL or the [AGPL-3] License. import ../types import vmath except Quat import arr_ref # import tinyre import std/tables import ../platform/gl # Forward declarations func mipmapHigh*(self: Texture): int proc needsMipmap*(self: Texture): bool proc setMaxTextures*(count: int32) proc newTextureStorage*(ttype: TextureType, width, height, depth: int, format: TextureFormat): TextureStorage proc preallocate(self: Texture) proc freeTextureStorage(self: Texture) proc bind_it*(texture: Texture, reserve_slot: static[int32] = -1, needs_active_texture: static[bool] = false) proc unbind*(texture: Texture) proc unbindAllTextures*() proc destroy*(texture: Texture) proc loadFromPixelsPointer*(self: Texture, pixels: pointer) proc loadFromPixels*[T](self: Texture, pixels: SliceMem[T]) {.gcsafe.} proc loadCubeSideFromPixels*(self: Texture, pixels: pointer, side: int32 = 0) proc setFilter*(self: Texture, filter: TextureFilter) proc newTexture*(engine: MyouEngine, name: string, width, height: int, depth: int = 1, format: TextureFormat, tex_type: TextureType = Tex2D, filter: TextureFilter = Trilinear, pixels: ArrRef[float32] = nil): Texture proc generateMipmap*(self: Texture) func to_sRGB*(format: TextureFormat): TextureFormat proc newTexture*(engine: MyouEngine, name: string, data: SliceMem[byte], is_sRGB: bool, filter: TextureFilter = Trilinear, depth=1, flip = true, use_compression = true): Texture proc newTexture*(engine: MyouEngine, name: string, file_name: string, is_sRGB: bool, filter: TextureFilter = Trilinear, tex_type: TextureType = Tex2D, flip = true, use_compression = true, ): Texture proc setExtrapolation*(self: Texture, ext: TextureExtrapolation) proc getTexturePixels*(self: Texture): TexturePixels proc setMipmapRange*(self: Texture, first = 0, last = 1000) {.gcsafe.} func vec3size*(self: Texture, mip_level = -1): Vec3 func toInternalFormat*(format: TextureFormat): GLenum # End forward declarations # import sugar import std/bitops import std/strformat import loadable import dds_ktx import ../gpu_formats/texture_decode import ../gpu_formats/texture_optimize # TODO: use and test destructor # NOTE: we moved this from the render manager to global state # (if we ever need multiple instances with different context # we should move them back) # NOTE: set as threadvar for optimization (and for having procs gcsafe # implicitely), we only use them in the main thread var bound_textures {.threadvar.}: seq[Texture] var active_texture {.threadvar.}: int32 var next_texture {.threadvar.}: int32 var max_textures {.threadvar.}: int32 var reserved {.threadvar.}: int32 active_texture = -1 if defined(android): # I don't know why slot 0 makes the mesh not render at all # TODO: maybe it only happens when building with ArCore support # even if it's not used, we should test it reserved = 1 func samplerType*(tex_type: TextureType): string = case tex_type: of Tex2D: "sampler2D" of Tex2DArray: "sampler2DArray" of Tex3D: "sampler3D" of TexCube: "samplerCube" of TexExternal: "samplerExternalOES" ## NOTE: requires GLSL extension template samplerType*(self: Texture): string = self.tex_type.samplerType func toInternalFormat*(format: TextureFormat): GLenum = case format: of SRGB_u8: GL_SRGB8 of SRGB_Alpha_u8: GL_SRGB8_ALPHA8 of R_u8: GL_R8 of RG_u8: GL_RG8 of RGB_u8: GL_RGB8 of RGBA_u8: GL_RGBA8 of R_u16: GL_R16_EXT of RG_u16: GL_RG16_EXT of RGB_u16: GL_RGB16_EXT of RGBA_u16: GL_RGBA16_EXT of R_f16: GL_R16F of RG_f16: GL_RG16F of RGB_f16: GL_RGB16F of RGBA_f16: GL_RGBA16F of R_f32: GL_R32F of RG_f32: GL_RG32F of RGB_f32: GL_RGB32F of RGBA_f32: GL_RGBA32F of Depth_u16: GL_DEPTH_COMPONENT16 of Depth_u24: GL_DEPTH_COMPONENT24 # of Depth_u24_s8: 0x81A7.GLenum of Depth_f32: GL_DEPTH_COMPONENT32F # else: # raise newException(ValueError, "Unsupported format") # To change with bindless textures func texturesNeedBinding*(): bool {.inline.} = true func mipmapHigh*(self: Texture): int = let depth = if self.tex_type == Tex2DArray: 1 else: self.depth floor(log2(max(max(self.width, self.height), depth).float32)).int func mipmapCount*(self: Texture): int = self.mipmapHigh + 1 proc needsMipmap*(self: Texture): bool = case self.filter: of Nearest, Linear: false else: true # TODO: this is hacky # instead we should query the GL API the first time we need this # or to make a texture manager class proc setMaxTextures*(count: int32) = max_textures = count assert max_textures != 0 bound_textures.setLen max_textures proc setTextureReservedSlots*(reserved_slots_bitfield: int32) = reserved = reserved_slots_bitfield proc resetNextTextureSlot*() = next_texture = 0 proc newTextureStorage*(ttype: TextureType, width, height, depth: int, format: TextureFormat): TextureStorage = var ts = new TextureStorage ts.unit = -1 ts.target = case ttype: of Tex2D: GL_TEXTURE_2D of Tex2DArray: GL_TEXTURE_2D_ARRAY of Tex3D: GL_TEXTURE_3D of TexCube: GL_TEXTURE_CUBE_MAP of TexExternal: GL_TEXTURE_EXTERNAL_OES ts.iformat = format.toInternalFormat ts.format = case format: of R_u8, R_u16, R_f16, R_f32: GL_RED of RG_u8, RG_u16, RG_f16, RG_f32: GL_RG of SRGB_u8, RGB_u8, RGB_u16, RGB_f16, RGB_f32: GL_RGB of SRGB_Alpha_u8, RGBA_u8, RGBA_u16, RGBA_f16, RGBA_f32: GL_RGBA of Depth_u16, Depth_u24, Depth_f32: GL_DEPTH_COMPONENT # of Depth_u24_s8: GL_DEPTH_COMPONENT # else: # raise newException(ValueError, "Unsupported format") ts.gltype = case format: of SRGB_u8, SRGB_Alpha_u8, R_u8, RG_u8, RGB_u8, RGBA_u8: GL_UNSIGNED_BYTE of R_f16, RG_f16, RGB_f16, RGBA_f16: GL_HALF_FLOAT of R_f32, RG_f32, RGB_f32, RGBA_f32, Depth_f32: cGL_FLOAT of R_u16, RG_u16, RGB_u16, RGBA_u16, Depth_u16: GL_UNSIGNED_SHORT of Depth_u24: GL_UNSIGNED_INT # of Depth_u24_s8: GL_UNSIGNED_INT ts.layer = 0 ts.tile_size = vec2(1,1) var tex: GLuint glGenTextures(1, addr tex) ts.tex = tex.GPUTexture return ts proc preallocate(self: Texture) = let ts {.cursor.} = self.storage case self.tex_type: of Tex2D: # TODO: only do this if necessary for m in 0 .. self.mipmapHigh: glTexImage2D(ts.target, m.GLint, ts.iformat.GLint, max(1, self.width.GLsizei shr m), max(1, self.height.GLsizei shr m), 0, ts.format, ts.gltype, nil) of Tex2DArray: # TODO: only do this if necessary for m in 0 .. self.mipmapHigh: glTexImage3D(ts.target, m.GLint, ts.iformat.GLint, max(1, self.width.GLsizei shr m), max(1, self.height.GLsizei shr m), self.depth.GLsizei, 0, ts.format, ts.gltype, nil) of Tex3D: # TODO: only do this if necessary for m in 0 .. self.mipmapHigh: glTexImage3D(ts.target, m.GLint, ts.iformat.GLint, max(1, self.width.GLsizei shr m), max(1, self.height.GLsizei shr m), max(1, self.depth.GLsizei shr m), 0, ts.format, ts.gltype, nil) of TexCube: for m in 0 .. self.mipmapHigh: for i in GL_TEXTURE_CUBE_MAP_POSITIVE_X.GLuint .. GL_TEXTURE_CUBE_MAP_NEGATIVE_Z.GLuint: glTexImage2D(i.GLenum, m.GLint, ts.iformat.GLint, max(1, self.width.GLsizei shr m), max(1, self.height.GLsizei shr m), 0, ts.format, ts.gltype, nil) of TexExternal: discard proc freeTextureStorage(self: Texture) = self.storage = nil proc bind_it*(texture: Texture, reserve_slot: static[int32] = -1, needs_active_texture: static[bool] = false) = # TODO: rewrite for using texture arrays and/or bindless if texture.storage.unit == -1: when reserve_slot >= 0: let bound_unit = reserve_slot reserved.setbit bound_unit else: while reserved.testbit next_texture: inc(next_texture) if next_texture == max_textures: next_texture = 0 let bound_unit = next_texture texture.storage.unit = bound_unit if active_texture != bound_unit: active_texture = bound_unit glActiveTexture(cast[GLenum](GL_TEXTURE0.uint32 + bound_unit.uint32)) var old_tex {.cursor.} = bound_textures[bound_unit] if old_tex != nil: old_tex.storage.unit = -1 if old_tex.storage.target.uint32 != texture.storage.target.uint32: glBindTexture(old_tex.storage.target, 0) # if old_tex.sampler_object != 0: # glBindSampler(cast[GLuint](bound_unit), 0) bound_textures[bound_unit] = nil glBindTexture(texture.storage.target, GLuint(texture.storage.tex)) # if texture.sampler_object != 0: # glBindSampler(bound_unit.GLuint, texture.sampler_object) bound_textures[bound_unit] = texture inc(next_texture) if next_texture == max_textures: next_texture = 0 else: when needs_active_texture: if active_texture != texture.storage.unit: active_texture = texture.storage.unit glActiveTexture((GL_TEXTURE0.uint32 + texture.storage.unit.uint32).GLenum) proc unbind*(texture: Texture) = if texture.storage.unit == -1: return let bound_unit = texture.storage.unit var old_tex {.cursor.} = bound_textures[bound_unit] assert old_tex == texture, "unexpected bound texture" if old_tex == texture: bound_textures[bound_unit] = nil active_texture = bound_unit glActiveTexture((GL_TEXTURE0.uint32 + bound_unit.uint32).GLenum) glBindTexture(texture.storage.target, 0) if texture.sampler_object != 0: glBindSampler(bound_unit.GLuint, 0) texture.storage.unit = -1 texture.last_used_shader = nil reserved.clearbit bound_unit proc bind_all*(textures: seq[Texture], locations: seq[GLint]) = # TODO: if not loaded, put a blank texture instead var used = reserved for t in textures: let unit = t.storage.unit if unit != -1: used.setbit unit for i,texture in textures: var unit = texture.storage.unit if unit == -1: while used.testbit next_texture: inc(next_texture) if next_texture == max_textures: next_texture = 0 unit = next_texture active_texture = unit texture.storage.unit = unit glActiveTexture(cast[GLenum](GL_TEXTURE0.uint32 + unit.uint32)) let old_tex {.cursor.} = bound_textures[unit] if old_tex != nil: old_tex.storage.unit = -1 if old_tex.storage.target != texture.storage.target: glBindTexture(old_tex.storage.target, 0) glBindTexture(texture.storage.target, texture.storage.tex.GLuint) bound_textures[unit] = texture inc(next_texture) if next_texture == max_textures: next_texture = 0 glUniform1i(locations[i], unit) proc unbindAllTextures*() = for tex in bound_textures: if tex != nil: tex.unbind() next_texture = 0 proc destroy*(texture: Texture) = if texture.storage != nil: texture.unbind() texture.freeTextureStorage() proc loadFromPixelsPointer*(self: Texture, pixels: pointer) = let ts = self.storage self.loaded = true self.bind_it(needs_active_texture=true) when not defined(release): assert self.tex_type notin [TexCube, TexExternal] if self.depth == 1: glTexImage2D(ts.target, 0, ts.iformat.GLint, self.width.GLsizei, self.height.GLsizei, 0, ts.format, ts.gltype, pixels) else: glTexImage3D(ts.target, 0, ts.iformat.GLint, self.width.GLsizei, self.height.GLsizei, self.depth.GLsizei, 0, ts.format, ts.gltype, pixels) if self.needsMipmap: glGenerateMipmap(ts.target) proc loadFromPixels*[T](self: Texture, pixels: SliceMem[T]) {.gcsafe.} = self.loadFromPixelsPointer(pixels.data) proc loadCubeSideFromPixels*(self: Texture, pixels: pointer, side: int32 = 0) = let ts = self.storage self.loaded = true self.bind_it(needs_active_texture=true) when not defined(release): assert self.tex_type == TexCube glTexImage2D((GL_TEXTURE_CUBE_MAP_POSITIVE_X.GLuint + cast[GLuint](side)).GLenum, 0, ts.iformat.GLint, self.width.GLsizei, self.height.GLsizei, 0, ts.format, ts.gltype, pixels) proc loadCompressedData*(self: Texture, data: KtxInfoParts, refdata: seq[SliceMem[byte]]) {.gcsafe.} = assert data.info.depth == 1 and data.info.num_layers == 1, "Compressed array and 3D textures not supported yet" let ts = self.storage self.loaded = true self.bind_it(needs_active_texture=true) let target = if self.tex_type == TexCube: GL_TEXTURE_CUBE_MAP_POSITIVE_X.GLuint.int32 else: ts.target.GLuint.int32 for part in data.parts: glCompressedTexImage2D(cast[GLenum](target+part.face), part.mip_level, data.info.internal_format.GLenum, part.width.GLsizei, part.height.GLsizei, 0, part.len.GLsizei, part.data) self.setMipmapRange(0, data.info.num_mipmaps - 1) proc setFilter*(self: Texture, filter: TextureFilter) = self.filter = filter self.engine.renderer.enqueue proc()= if self.storage == nil: return let min_filter = case filter of Nearest: GL_NEAREST of Pixellated: GL_NEAREST_MIPMAP_NEAREST of Linear: GL_LINEAR of Trilinear, Anisotropic: GL_LINEAR_MIPMAP_LINEAR let mag_filter = case filter of Nearest, Pixellated: GL_NEAREST else: GL_LINEAR self.bind_it(needs_active_texture=true) glTexParameteri(self.storage.target, GL_TEXTURE_MAG_FILTER, mag_filter.GLint) glTexParameteri(self.storage.target, GL_TEXTURE_MIN_FILTER, min_filter.GLint) proc ensure_storage*(self: Texture) = if self.storage == nil: self.storage = newTextureStorage(self.tex_type, self.width, self.height, self.depth, self.format) self.setFilter self.filter self.mipmap_range = (0, self.mipmapHigh) proc newTexture*(engine: MyouEngine, name: string, width, height: int; depth: int = 1, format: TextureFormat, tex_type: TextureType = Tex2D, filter: TextureFilter = Trilinear, pixels: ArrRef[float32] = nil): Texture = result = new Texture result.engine = engine result.name = name result.tex_type = tex_type result.format = format result.width = width result.height = height result.depth = depth if tex_type == TexCube: assert width == height result.depth = width result.filter = filter if width != 0 and height != 0: let self = result self.engine.renderer.enqueue proc()= self.ensure_storage() if pixels != nil: self.loadFromPixelsPointer(pixels.toPointer) else: self.preallocate() when defined(myouUseRenderdoc): # Note: when we switch to arrays we'll have to store resolutions # instead of names glObjectLabel(GL_TEXTURE, self.storage.tex.GLuint, GLsizei(self.name.len), self.name.cstring) proc generateMipmap*(self: Texture) = assert self.tex_type != TexExternal self.bind_it(needs_active_texture=true) glGenerateMipmap(self.storage.target) func to_sRGB*(format: TextureFormat): TextureFormat = return case format: of RGBA_u8: SRGB_Alpha_u8 of RGB_u8: SRGB_u8 else: raise newException(ValueError, "There's no sRGB version of " & $format) proc newTexture*(engine: MyouEngine, name: string, data: SliceMem[byte], is_sRGB: bool, filter: TextureFilter = Trilinear, depth=1, flip = true, use_compression = true): Texture = var (width, height, format) = getDimensionsFormat(data.data, data.byte_len) if is_sRGB: if format in [RGB_u8, RGBA_u8]: format = format.to_sRGB else: # TODO: sRGB shader for 1 and 2 channel textures echo &"WARNING: Texture {name} is sRGB but has format {format} _" let self = engine.newTexture(name, width, height, depth, format, filter=filter) self.is_sRGB = is_sRGB engine.renderer.enqueue proc() = if use_compression: self.loadOptimizedThreaded(@[data], loadFromPixels, loadCompressedData) else: self.loadOptimizedThreaded(@[data], loadFromPixels, nil) return self proc newTexture*(engine: MyouEngine, name: string, file_name: string, is_sRGB: bool, filter: TextureFilter = Trilinear, tex_type: TextureType = Tex2D, flip = true, use_compression = true, ): Texture = # TODO: Api that stores the LoadableResource so it can be re-loaded later # note: format does not matter at this point, will be replaced later let self = engine.newTexture(name, 0, 0, filter=filter, format=RGBA_u8, tex_type=tex_type) self.is_sRGB = is_sRGB var res: LoadableResource proc load(ok: bool, err: string, data: SliceMem[byte]) = assert ok, &"Error loading texture '{self.name}' from '{file_name}': {err}" let ktx_info = GetDdsKtxInfo(data.data, data.byte_len) if ktx_info.isSome: let ktx_info = ktx_info.get assert ktx_info.is_cubemap == (self.tex_type == TexCube) self.width = ktx_info.width self.height = ktx_info.height self.depth = ktx_info.depth self.format = if ktx_info.has_alpha: RGBA_u8 else: RGB_u8 if ktx_info.is_sRGB: self.format = self.format.to_sRGB engine.renderer.enqueue proc()= try: self.ensure_storage() let info_parts = KtxInfoParts( info: ktx_info, parts: ParseDdsKtx(data.data, data.byte_len)) self.loadCompressedData(info_parts, @[data]) self.loaded = true # except Exception as e: except: # TODO: use logging # for line in e.getStackTrace.split '\n': # echo line echo getCurrentExceptionMsg() echo "Error loading texture " & file_name else: var (width, height, format) = getDimensionsFormat(data.data, data.byte_len, 3) if is_sRGB: if format in [RGB_u8, RGBA_u8]: format = format.to_sRGB else: # TODO: sRGB shader for 1 and 2 channel textures echo &"WARNING: Texture {self.name} is sRGB but has format {format}" self.width = width self.height = height self.format = format engine.renderer.enqueue proc()= try: self.ensure_storage() if use_compression: self.loadOptimizedThreaded(@[data], loadFromPixels, loadCompressedData) else: self.loadOptimizedThreaded(@[data], loadFromPixels, nil) except: # TODO: use logging echo getCurrentExceptionMsg() echo "Error loading texture " & file_name res = file_name.loadUri(load, auto_start=false) res.start() return self proc setExtrapolation*(self: Texture, ext: TextureExtrapolation) = let e = case ext: of Clamp: GL_CLAMP_TO_EDGE of Border: GL_CLAMP_TO_BORDER of Repeat: GL_REPEAT of MirroredRepeat: GL_MIRRORED_REPEAT self.engine.renderer.enqueue proc()= self.bind_it(needs_active_texture=true) glTexParameteri(self.storage.target, GL_TEXTURE_WRAP_S, e.GLint) glTexParameteri(self.storage.target, GL_TEXTURE_WRAP_T, e.GLint) glTexParameteri(self.storage.target, GL_TEXTURE_WRAP_R, e.GLint) proc getTexturePixels*(self: Texture): TexturePixels = when false: var len_bytes, cube_stride = self.width * self.height * self.format.stride if self.tex_type == TexCube: len_bytes *= 6 result.pixels = newArrRef[int8](len_bytes).to(float32) result.width = self.width result.height = self.height result.format = self.format result.tex_type = self.tex_type self.bind_it(needs_active_texture=true) if self.tex_type == TexCube: var p = cast[ptr UncheckedArray[int8]](result.pixels.toPointer) var pos = 0 for i in GL_TEXTURE_CUBE_MAP_POSITIVE_X.GLuint .. GL_TEXTURE_CUBE_MAP_NEGATIVE_Z.GLuint: glGetTexImage(i.GLenum, 0, self.storage.format, self.storage.gltype, p[pos].addr) pos += cube_stride else: glGetTexImage(self.storage.target, 0, self.storage.format, self.storage.gltype, result.pixels.toPointer) # TODO: limit the amount of mip maps to save some draw calls # (e.g. minimum resolution 16x16 for blurred mipmaps) proc setMipmapRange*(self: Texture, first = 0, last = 1000) {.gcsafe.} = self.bind_it(needs_active_texture=true) self.mipmap_range = (first, last) glTexParameteri(self.storage.target, GL_TEXTURE_BASE_LEVEL, first.GLint); glTexParameteri(self.storage.target, GL_TEXTURE_MAX_LEVEL, last.GLint); func vec3size*(self: Texture, mip_level = -1): Vec3 = let depth = if self.tex_type == Tex2DArray: 1 else: self.depth let mip_level = if mip_level != -1: mip_level else: self.mipmap_range[0] vec3((max(1, self.width shr mip_level)).float32, (max(1, self.height shr mip_level)).float32, (max(1, depth shr mip_level)).float32) proc set_texture_shadow*(self: Texture) = self.bind_it(needs_active_texture=true) if self.format in [Depth_u16, Depth_u24, Depth_f32]: glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE.GLint) # TODO: would GL_LESS be better? That way depth 1.0 would always be not in shadow # so we wouldn't need to clamp in shader glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL.GLint) proc unset_texture_shadow*(self: Texture) = self.bind_it(needs_active_texture=true) glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_MODE, GL_NONE.GLint)