#include "precompiled.h" #include "../res.h" #include "ogl.h" #include "tex.h" #include "ogl_tex.h" #include "lib.h" static GLint default_filter = GL_LINEAR; // all legal GL *minify* values static uint default_bpp = 32; // 16 or 32 //---------------------------------------------------------------------------- // OpenGL helper routines //---------------------------------------------------------------------------- static bool filter_is_known(GLint filter) { switch(filter) { case GL_NEAREST: case GL_LINEAR: case GL_NEAREST_MIPMAP_NEAREST: case GL_LINEAR_MIPMAP_NEAREST: case GL_NEAREST_MIPMAP_LINEAR: case GL_LINEAR_MIPMAP_LINEAR: return true; } return false; } static bool filter_uses_mipmaps(GLint filter) { switch(filter) { case GL_NEAREST_MIPMAP_NEAREST: case GL_LINEAR_MIPMAP_NEAREST: case GL_NEAREST_MIPMAP_LINEAR: case GL_LINEAR_MIPMAP_LINEAR: return true; default: return false; } } // determine OpenGL texture format, given and Tex . // also choose an internal format based on the global // performance vs. quality setting. // // rationale: we override the user's previous internal format preference. // this is reasonable: 1) internal format is mostly performance optimization // 2) if it does turn out to be significant, better to reevaluate the // format decision after a reload than keep the user's setting. static int get_gl_fmt(int bpp, int flags, GLenum* fmt, GLint* int_fmt) { const bool alpha = (flags & TEX_ALPHA) != 0; const bool bgr = (flags & TEX_BGR ) != 0; const bool grey = (flags & TEX_GREY ) != 0; const int dxt = flags & TEX_DXT; // in case we fail *fmt = 0; *int_fmt = 0; // S3TC if(dxt != 0) { switch(dxt) { case 1: *fmt = alpha? GL_COMPRESSED_RGBA_S3TC_DXT1_EXT : GL_COMPRESSED_RGB_S3TC_DXT1_EXT; break; case 3: *fmt = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; break; case 5: *fmt = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; break; default: debug_warn("get_gl_fmt: invalid DXT value"); return ERR_TEX_FMT_INVALID; } // note: S3TC textures don't need an internal format, since they're // uploaded via glCompressedTexImage2DARB. we'll set it anyway for // consistency. *int_fmt = *fmt; return 0; } // true => 8 bits per component; otherwise, 4 const bool high_quality = (default_bpp == 32); switch(bpp) { case 8: debug_assert(grey); *fmt = GL_LUMINANCE; *int_fmt = high_quality? GL_LUMINANCE8 : GL_LUMINANCE4; return 0; case 16: *fmt = GL_LUMINANCE_ALPHA; *int_fmt = high_quality? GL_LUMINANCE8_ALPHA8 : GL_LUMINANCE4_ALPHA4; return 0; case 24: debug_assert(!alpha); *fmt = bgr? GL_BGR : GL_RGB; // note: BGR can't be used as internal format *int_fmt = high_quality? GL_RGB8 : GL_RGB4; return 0; case 32: debug_assert(alpha); *fmt = bgr? GL_BGRA : GL_RGBA; // note: BGR can't be used as internal format *int_fmt = high_quality? GL_RGBA8 : GL_RGBA4; return 0; default: debug_warn("get_gl_fmt: invalid bpp"); return ERR_TEX_FMT_INVALID; } UNREACHABLE; } // return a token for glTexParameteri that will enable automatic mipmap // generation, or GL_FALSE if the GL implementation doesn't support it. // does not cache the result. // // rationale: we don't assume GL_GENERATE_MIPMAP and GL_GENERATE_MIPMAP_SGIS // have the same values, although this is implied by the spec governing // 'promoted' ARB extensions. checking for both the old extension and // core 1.4 is future-proof. static GLint detect_auto_mipmap_gen() { // OpenGL 1.4 core. we don't assume this is supported by the driver, // since software implementations usually only have 1.1. if(oglHaveVersion("1.4")) return GL_GENERATE_MIPMAP; // widespread extension if(oglHaveExtension("GL_SGIS_generate_mipmap")) return GL_GENERATE_MIPMAP_SGIS; // neither supported; need to build manually, e.g. with gluBuild2DMipmaps. return GL_FALSE; } // change default upload settings - these affect performance vs. quality. // may be overridden for individual textures via ogl_tex_upload parameters. // pass 0 to keep the current setting; defaults and legal values are: // - filter: GL_LINEAR; any valid OpenGL minification filter // - bpp : 32; 16 or 32 (this toggles between RGBA4 and RGBA8) void ogl_tex_set_default_upload(GLint filter, uint bpp) { if(filter) { debug_assert(filter_is_known(filter)); default_filter = filter; } if(bpp) { debug_assert(bpp == 16 || bpp == 32); default_bpp = bpp; } } //---------------------------------------------------------------------------- // texture resource implementation //---------------------------------------------------------------------------- // ideally we would split OglTex into data and state objects as in // snd.cpp's SndData / VSrc. this gives us the benefits of caching while // still leaving each "instance" (state object, which owns a data reference) // free to change its state. however, unlike in OpenAL, there is no state // independent of the data object - all parameters are directly tied to the // GL texture object. therefore, splitting them up is impossible. // (we shouldn't even keep the texel data in memory since that's already // covered by the FS cache). // // given that multiple "instances" share the state stored here, we conclude: // - a refcount is necessary to prevent ogl_tex_upload from freeing // as long as other instances are active. // - concurrent use risks cross-talk (if the 2nd "instance" changes state and // the first is reloaded, its state may change to that of the 2nd) // // as bad as it sounds, the latter issue isn't a problem: // multiple instances of the same texture where someone changes its // internal format aren't expected. even if it is reloaded, the differing // state is not critical. // the alternative is even worse: disabling *all* caching/reuse would // really hurt performance and h_mgr doesn't support disallowing reuse of // active objects only (would break the index lookup code, since // multiple instances may exist). struct OglTex { Tex t; // allocated by OglTex_reload; indicates the texture is currently uploaded. GLuint id; // determined from Tex by gl_get_fmt (called from OglTex_reload); // user settings passed to ogl_tex_upload will override this until the // next actual reload. GLenum fmt; GLint int_fmt; // set to by OglTex_init; user settings passed to // ogl_tex_upload will permanently override this. GLint filter; // flags influencing reload behavior // .. either we have the texture in memory (referenced by t.hm), // or it's already been uploaded to OpenGL => no reload necessary. // needs to be a flag so it can be reset in Tex_dtor. uint is_loaded : 1; uint has_been_uploaded : 1; uint was_wrapped : 1; // to which Texture Mapping Unit was this bound? // used when re-binding after reload. uint tmu : 8; }; H_TYPE_DEFINE(OglTex); static void OglTex_init(OglTex* ot, va_list args) { Tex* wrapped_tex = va_arg(args, Tex*); if(wrapped_tex) { ot->t = *wrapped_tex; ot->was_wrapped = 1; } // set to default (once) ot->filter = default_filter; } static void OglTex_dtor(OglTex* ot) { (void)tex_free(&ot->t); glDeleteTextures(1, &ot->id); ot->id = 0; // need to clear this so actual reloads (triggered by h_reload) // actually reload. ot->is_loaded = 0; } static int OglTex_reload(OglTex* ot, const char* fn, Handle h) { if(ot->is_loaded) return 0; Tex* const t = &ot->t; if(!ot->was_wrapped) RETURN_ERR(tex_load(fn, &ot->t)); // always override previous settings, since format in // texture file may have changed (e.g. 24 -> 32 bpp). CHECK_ERR(get_gl_fmt(t->bpp, t->flags, &ot->fmt, &ot->int_fmt)); ot->is_loaded = 1; glGenTextures(1, &ot->id); // re-upload if necessary if(ot->has_been_uploaded) CHECK_ERR(ogl_tex_upload(h, ot->filter, ot->int_fmt)); return 0; } // load and return a handle to the texture given in . // for a list of supported formats, see tex.h's tex_load. Handle ogl_tex_load(const char* fn, uint flags) { Tex* wrapped_tex = 0; // we're loading from file return h_alloc(H_OglTex, fn, flags, wrapped_tex); } // make the given Tex object ready for use as an OpenGL texture // and return a handle to it. this will be as if its contents // had been loaded by ogl_tex_load. // // we need only add bookkeeping information and "wrap" it in // a resource object (accessed via Handle), hence the name. // // isn't strictly needed but should describe the texture so that // h_filename will return a meaningful comment for debug purposes. Handle ogl_tex_wrap(Tex* t, const char* fn, uint flags) { return h_alloc(H_OglTex, fn, flags, t); } // free all resources associated with the texture and make further // use of it impossible. (subject to refcount) int ogl_tex_free(Handle& ht) { return h_free(ht, H_OglTex); } static int ogl_tex_validate(const uint line, const OglTex* ot) { RETURN_ERR(tex_validate(line, &ot->t)); const char* msg = 0; int err = -1; // width, height // (note: this is done here because tex.cpp doesn't impose any // restrictions on dimensions, while OpenGL does). GLsizei w = (GLsizei)ot->t.w; GLsizei h = (GLsizei)ot->t.h; // if w or h is 0, texture file probably not loaded successfully. if(w == 0 || h == 0) msg = "width or height is 0 - texture probably not loaded successfully"; // greater than max supported tex dimension? // no-op if oglInit not yet called if(w > (GLsizei)ogl_max_tex_size || h > (GLsizei)ogl_max_tex_size) msg = "texture dimensions exceed OpenGL implementation limit"; // both NV_texture_rectangle and subtexture require work for the client // (changing tex coords) => we'll just disallow non-power of 2 textures. // TODO: ARB_texture_non_power_of_two if(!is_pow2(w) || !is_pow2(h)) msg = "width or height is not a power-of-2"; // texel format GLenum fmt = (GLenum)ot->fmt; if(!fmt) msg = "texel format is 0"; // can't really check against a list of valid formats - loaders // may define their own. not necessary anyway - if non-0, assume // loader knows what it's doing, and that the format is valid. // upload parameters, set by ogl_tex_upload(Handle), or 0 GLint filter = ot->filter; if(filter != 0 && !filter_is_known(filter)) msg = "invalid filter"; // as with the texel format above, there is not anything we can do // to verify ot->int_fmt is correct (even 0 is valid). if(ot->tmu >= 128) msg = "TMU invalid? it's >= 128!"; if(msg) { debug_printf("ogl_tex_validate at line %d failed: %s (error code %d)\n", line, msg, err); debug_warn("ogl_tex_validate failed"); return err; } return 0; } #define CHECK_OGL_TEX(ot) CHECK_ERR(ogl_tex_validate(__LINE__, ot)) // upload the texture to OpenGL. texture filter and [internal] format // may be specified to override the global defaults (see below). // side effects: // - enables texturing on TMU 0 and binds the texture to it; // - frees the texel data! see ogl_tex_get_data. int ogl_tex_upload(const Handle ht, GLint filter_ovr, GLint int_fmt_ovr, GLenum fmt_ovr) { H_DEREF(ht, OglTex, ot); CHECK_OGL_TEX(ot); // someone's requesting upload, but has already been uploaded. // this happens if a cached texture is "loaded". no work to do. if(ot->id && ot->t.hm <= 0) return 0; CHECK_OGL_TEX(ot); // must come after check above to avoid false alarms const char* fn = h_filename(ht); if(!fn) { fn = "(could not determine filename)"; debug_warn("ogl_tex_upload(Handle): h_filename failed"); } // allow user override of format/settings if(filter_ovr) ot->filter = filter_ovr; if(int_fmt_ovr) ot->int_fmt = int_fmt_ovr; if(fmt_ovr) ot->fmt = fmt_ovr; // convenient local copies. note: have been validated by CHECK_TEX. GLsizei w = (GLsizei)ot->t.w; GLsizei h = (GLsizei)ot->t.h; u32 bpp = ot->t.bpp; // used for S3TC/mipmap size calc GLenum fmt = ot->fmt; GLint filter = ot->filter; GLint int_fmt = ot->int_fmt; void* tex_data = tex_get_data(&ot->t); // does filter call for uploading mipmaps? const bool need_mipmaps = filter_uses_mipmaps(filter); static GLint auto_mipmap_gen; ONCE(auto_mipmap_gen = detect_auto_mipmap_gen()); // we know ht is valid (H_DEREF above), but ogl_tex_bind can // fail in debug builds if OglTex.id isn't a valid texture name CHECK_ERR(ogl_tex_bind(ht, ot->tmu)); // set upload params glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); const GLint mag_filter = (filter == GL_NEAREST)? GL_NEAREST : GL_LINEAR; // magnify can only be linear or nearest glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter); /* There are various combinations of desires/abilities, relating to how (and whether) mipmaps should be generated. Currently there are only 2^4 such combinations: /mipmaps available in texture ####### /mipmaps needed ####### .---+---+---+---. | Au| Mu| Nu| Nu|#-auto mipmap generation available |---+---+---+---|# | Ac| Mc| Nc| Nc|# #-texture is compressed |---+---+---+---|# # | X | Mc| Nc| Nc| # |---+---+---+---| # | G | Mu| Nu| Nu| `---+---+---+---' Au (auto_uncomp) = auto_mipmap_gen, then 'Nu' Ac (auto_comp) = auto_mipmap_gen, then 'Nc' X (broken_comp) = failure; just fall back to GL_LINEAR and 'Nc' G (glubuild) = gluBuild2DMipmaps Mu (mipped_uncomp) = glTexImage2D, mipmap levels Mc (mipped_comp) = glCompressedTexImage2DARB, mipmap levels Nu (normal_uncomp) = glTexImage2D Nc (normal_comp) = glCompressedTexImage2DARB if (Au || Ac) enable automatic mipmap generation switch to 'N*' if (X) set GL_LINEAR switch to 'Nc' if (G) gluBuild2DMipmaps if (Nu) glTexImage2D if (Nc) glCompressedTexImage2DARB if (Mu) for each mipmap level glTexImage2D if (Mc) for each mipmap level glCompressedTexImage2DARB [This documentation feels like more than is really necessary, but hopefully it'll prevent the logic getting horribly tangled...] */ const bool is_s3tc = ogl_dxt_from_fmt(fmt) != 0; const bool has_mipmaps = (ot->t.flags & TEX_MIPMAPS) != 0; enum UploadState { auto_uncomp, auto_comp, mipped_uncomp, mipped_comp, normal_uncomp, normal_comp, broken_comp, glubuild }; static const int states[4][4] = { { auto_uncomp, mipped_uncomp, normal_uncomp, normal_uncomp }, { auto_comp, mipped_comp, normal_comp, normal_comp }, { broken_comp, mipped_comp, normal_comp, normal_comp }, { glubuild, mipped_uncomp, normal_uncomp, normal_uncomp } }; int state = states[auto_mipmap_gen ? (is_s3tc ? 1 : 0) : (is_s3tc ? 2 : 3)] // row [need_mipmaps ? (has_mipmaps ? 1 : 0) : (has_mipmaps ? 2 : 3)]; // column if(state == auto_uncomp || state == auto_comp) { glTexParameteri(GL_TEXTURE_2D, auto_mipmap_gen, GL_TRUE); state = (state == auto_uncomp)? normal_uncomp : normal_comp; } if(state == broken_comp) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); state = normal_comp; } if(state == glubuild) gluBuild2DMipmaps(GL_TEXTURE_2D, int_fmt, w, h, fmt, GL_UNSIGNED_BYTE, tex_data); else if(state == normal_uncomp) glTexImage2D(GL_TEXTURE_2D, 0, int_fmt, w, h, 0, fmt, GL_UNSIGNED_BYTE, tex_data); else if(state == normal_comp) { const GLsizei tex_size = w * h * bpp / 8; glCompressedTexImage2DARB(GL_TEXTURE_2D, 0, fmt, w, h, 0, tex_size, tex_data); } else if(state == mipped_uncomp || state == mipped_comp) { int level = 0; GLsizei level_w = w; GLsizei level_h = h; char* mipmap_data = (char*)tex_data; while(level_w || level_h) // loop until the 1x1 mipmap level has just been processed { // If the texture is non-square, one of the dimensions will become // 0 before the other. To satisfy OpenGL's expectations, change it // back to 1. if(level_w == 0) level_w = 1; if(level_h == 0) level_h = 1; GLsizei tex_size; if(state == mipped_uncomp) { tex_size = level_w * level_h * bpp/8; glTexImage2D(GL_TEXTURE_2D, level, int_fmt, level_w, level_h, 0, fmt, GL_UNSIGNED_BYTE, mipmap_data); } else // state == mipped_comp { // Round up to an integer number of 4x4 blocks tex_size = (GLsizei)(round_up(level_w, 4) * round_up(level_h, 4) * bpp/8); glCompressedTexImage2DARB(GL_TEXTURE_2D, level, fmt, level_w, level_h, 0, tex_size, mipmap_data); } mipmap_data += tex_size; level++; level_w /= 2; level_h /= 2; } } else debug_warn("Invalid state in ogl_tex_upload"); // see rationale at declaration of OglTex int refs = h_get_refcnt(ht); if(refs > 0) tex_free(&ot->t); ot->has_been_uploaded = 1; oglCheck(); return 0; } /* ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // BindTexture: bind a GL texture object to current active unit void CRenderer::BindTexture(int unit,GLuint tex) { #if 0 glActiveTextureARB(GL_TEXTURE0+unit); if (tex==m_ActiveTextures[unit]) return; if (tex) { glBindTexture(GL_TEXTURE_2D,tex); if (!m_ActiveTextures[unit]) { glEnable(GL_TEXTURE_2D); } } else if (m_ActiveTextures[unit]) { glDisable(GL_TEXTURE_2D); } m_ActiveTextures[unit]=tex; #endif glActiveTextureARB(GL_TEXTURE0+unit); glBindTexture(GL_TEXTURE_2D,tex); if (tex) { glEnable(GL_TEXTURE_2D); } else { glDisable(GL_TEXTURE_2D); } m_ActiveTextures[unit]=tex; } */ // bind the texture to the specified unit [number] in preparation for // using it in rendering. assumes multitexturing is available. // side effects: // - enables (or disables, if == 0) texturing on the given unit. // // note: there are many call sites of glActiveTextureARB, so caching // those and ignoring redundant sets isn't feasible. int ogl_tex_bind(const Handle ht, GLenum unit) { int id = 0; // special case: avoid dereference and disable texturing directly. if(ht == 0) goto disable_texturing; { // (we can't use H_DEREF because it exits immediately) OglTex* ot = H_USER_DATA(ht, OglTex); if(!ot) { glBindTexture(GL_TEXTURE_2D, 0); CHECK_ERR(ERR_INVALID_HANDLE); UNREACHABLE; } CHECK_OGL_TEX(ot); #ifndef NDEBUG if(!ot->id) { debug_warn("ogl_tex_bind: OglTex.id is not a valid texture"); return -1; } #endif id = ot->id; ot->tmu = unit; } disable_texturing: glActiveTextureARB(GL_TEXTURE0+unit); glBindTexture(GL_TEXTURE_2D, id); if(id) glEnable(GL_TEXTURE_2D); else glDisable(GL_TEXTURE_2D); return 0; } //---------------------------------------------------------------------------- // retrieve texture dimensions and bits per pixel. // all params are optional and filled if non-NULL. int ogl_tex_get_size(Handle ht, int* w, int* h, int* bpp) { H_DEREF(ht, OglTex, ot); CHECK_OGL_TEX(ot); if(w) *w = ot->t.w; if(h) *h = ot->t.h; if(bpp) *bpp = ot->t.bpp; return 0; } // retrieve Tex.flags and the corresponding OpenGL format. // all params are optional and filled if non-NULL. int ogl_tex_get_format(Handle ht, int* flags, GLenum* fmt) { H_DEREF(ht, OglTex, ot); CHECK_OGL_TEX(ot); if(flags) *flags = ot->t.flags; if(fmt) *fmt = ot->fmt; return 0; } // retrieve pointer to texel data. // // note: this memory is freed after a successful ogl_tex_upload for // this texture. after that, the pointer we retrieve is NULL but // the function doesn't fail (negative return value) by design. // if you still need to get at the data, add a reference before // uploading it or read directly from OpenGL (discouraged). int ogl_tex_get_data(Handle ht, void** p) { H_DEREF(ht, OglTex, ot); CHECK_OGL_TEX(ot); *p = tex_get_data(&ot->t); return 0; }