/** * ========================================================================= * File : tex.cpp * Project : 0 A.D. * Description : support routines for 2d texture access/writing. * ========================================================================= */ // license: GPL; see lib/license.txt #include "precompiled.h" #include "tex.h" #include #include #include #include "lib/timer.h" #include "lib/bits.h" #include "tex_codec.h" ERROR_ASSOCIATE(ERR::TEX_FMT_INVALID, "Invalid/unsupported texture format", -1); ERROR_ASSOCIATE(ERR::TEX_INVALID_COLOR_TYPE, "Invalid color type", -1); ERROR_ASSOCIATE(ERR::TEX_NOT_8BIT_PRECISION, "Not 8-bit channel precision", -1); ERROR_ASSOCIATE(ERR::TEX_INVALID_LAYOUT, "Unsupported texel layout, e.g. right-to-left", -1); ERROR_ASSOCIATE(ERR::TEX_COMPRESSED, "Unsupported texture compression", -1); ERROR_ASSOCIATE(WARN::TEX_INVALID_DATA, "Warning: invalid texel data encountered", -1); ERROR_ASSOCIATE(ERR::TEX_INVALID_SIZE, "Texture size is incorrect", -1); ERROR_ASSOCIATE(INFO::TEX_CODEC_CANNOT_HANDLE, "Texture codec cannot handle the given format", -1); //----------------------------------------------------------------------------- // validation //----------------------------------------------------------------------------- // be careful not to use other tex_* APIs here because they call us. LibError tex_validate(const Tex* t) { if(t->flags & TEX_UNDEFINED_FLAGS) WARN_RETURN(ERR::_1); // pixel data (only check validity if the image is still in memory; // ogl_tex frees the data after uploading to GL) if(t->data) { // file size smaller than header+pixels. // possible causes: texture file header is invalid, // or file wasn't loaded completely. if(t->dataSize < t->ofs + t->w*t->h*t->bpp/8) WARN_RETURN(ERR::_2); } // bits per pixel // (we don't bother checking all values; a sanity check is enough) if(t->bpp % 4 || t->bpp > 32) WARN_RETURN(ERR::_3); // flags // .. DXT value const uint dxt = t->flags & TEX_DXT; if(dxt != 0 && dxt != 1 && dxt != DXT1A && dxt != 3 && dxt != 5) WARN_RETURN(ERR::_4); // .. orientation const uint orientation = t->flags & TEX_ORIENTATION; if(orientation == (TEX_BOTTOM_UP|TEX_TOP_DOWN)) WARN_RETURN(ERR::_5); return INFO::OK; } #define CHECK_TEX(t) RETURN_ERR(tex_validate(t)) // check if the given texture format is acceptable: 8bpp grey, // 24bpp color or 32bpp color+alpha (BGR / upside down are permitted). // basically, this is the "plain" format understood by all codecs and // tex_codec_plain_transform. LibError tex_validate_plain_format(uint bpp, uint flags) { const bool alpha = (flags & TEX_ALPHA ) != 0; const bool grey = (flags & TEX_GREY ) != 0; const bool dxt = (flags & TEX_DXT ) != 0; const bool mipmaps = (flags & TEX_MIPMAPS) != 0; if(dxt || mipmaps) WARN_RETURN(ERR::TEX_FMT_INVALID); // grey must be 8bpp without alpha, or it's invalid. if(grey) { if(bpp == 8 && !alpha) return INFO::OK; WARN_RETURN(ERR::TEX_FMT_INVALID); } if(bpp == 24 && !alpha) return INFO::OK; if(bpp == 32 && alpha) return INFO::OK; WARN_RETURN(ERR::TEX_FMT_INVALID); } //----------------------------------------------------------------------------- // mipmaps //----------------------------------------------------------------------------- void tex_util_foreach_mipmap(uint w, uint h, uint bpp, const u8* pixels, int levels_to_skip, uint data_padding, MipmapCB cb, void* RESTRICT cbData) { debug_assert(levels_to_skip >= 0 || levels_to_skip == TEX_BASE_LEVEL_ONLY); uint level_w = w, level_h = h; const u8* level_data = pixels; // we iterate through the loop (necessary to skip over image data), // but do not actually call back until the requisite number of // levels have been skipped (i.e. level == 0). int level = (levels_to_skip == TEX_BASE_LEVEL_ONLY)? 0 : -levels_to_skip; // until at level 1x1: for(;;) { // used to skip past this mip level in const size_t level_data_size = (size_t)(round_up(level_w, data_padding) * round_up(level_h, data_padding) * bpp/8); if(level >= 0) cb((uint)level, level_w, level_h, level_data, level_data_size, cbData); level_data += level_data_size; // 1x1 reached - done if(level_w == 1 && level_h == 1) break; level_w /= 2; level_h /= 2; // 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; level++; // special case: no mipmaps, we were only supposed to call for // the base level if(levels_to_skip == TEX_BASE_LEVEL_ONLY) break; } } struct CreateLevelData { uint num_components; uint prev_level_w; uint prev_level_h; const u8* prev_level_data; size_t prev_level_data_size; }; // uses 2x2 box filter static void create_level(uint level, uint level_w, uint level_h, const u8* RESTRICT level_data, size_t level_data_size, void* RESTRICT cbData) { CreateLevelData* cld = (CreateLevelData*)cbData; const size_t src_w = cld->prev_level_w; const size_t src_h = cld->prev_level_h; const u8* src = cld->prev_level_data; u8* dst = (u8*)level_data; // base level - must be copied over from source buffer if(level == 0) { debug_assert(level_data_size == cld->prev_level_data_size); cpu_memcpy(dst, src, level_data_size); } else { const uint num_components = cld->num_components; const size_t dx = num_components, dy = dx*src_w; // special case: image is too small for 2x2 filter if(cld->prev_level_w == 1 || cld->prev_level_h == 1) { // image is either a horizontal or vertical line. // their memory layout is the same (packed pixels), so no special // handling is needed; just pick max dimension. for(uint y = 0; y < std::max(src_w, src_h); y += 2) { for(uint i = 0; i < num_components; i++) { *dst++ = (src[0]+src[dx]+1)/2; src += 1; } src += dx; // skip to next pixel (since box is 2x2) } } // normal else { for(uint y = 0; y < src_h; y += 2) { for(uint x = 0; x < src_w; x += 2) { for(uint i = 0; i < num_components; i++) { *dst++ = (src[0]+src[dx]+src[dy]+src[dx+dy]+2)/4; src += 1; } src += dx; // skip to next pixel (since box is 2x2) } src += dy; // skip to next row (since box is 2x2) } } debug_assert(dst == level_data + level_data_size); debug_assert(src == cld->prev_level_data + cld->prev_level_data_size); } cld->prev_level_data = level_data; cld->prev_level_data_size = level_data_size; cld->prev_level_w = level_w; cld->prev_level_h = level_h; } static LibError add_mipmaps(Tex* t, uint w, uint h, uint bpp, void* newData, size_t data_size) { // this code assumes the image is of POT dimension; we don't // go to the trouble of implementing image scaling because // the only place this is used (ogl_tex_upload) requires POT anyway. if(!is_pow2(w) || !is_pow2(h)) WARN_RETURN(ERR::TEX_INVALID_SIZE); t->flags |= TEX_MIPMAPS; // must come before tex_img_size! const size_t mipmap_size = tex_img_size(t); shared_ptr mipmapData = io_Allocate(mipmap_size, 0); CreateLevelData cld = { bpp/8, w, h, (const u8*)newData, data_size }; tex_util_foreach_mipmap(w, h, bpp, mipmapData.get(), 0, 1, create_level, &cld); t->data = mipmapData; t->dataSize = mipmap_size; t->ofs = 0; return INFO::OK; } //----------------------------------------------------------------------------- // pixel format conversion (transformation) //----------------------------------------------------------------------------- TIMER_ADD_CLIENT(tc_plain_transform); // handles BGR and row flipping in "plain" format (see below). // // called by codecs after they get their format-specific transforms out of // the way. note that this approach requires several passes over the image, // but is much easier to maintain than providing all<->all conversion paths. // // somewhat optimized (loops are hoisted, cache associativity accounted for) static LibError plain_transform(Tex* t, uint transforms) { TIMER_ACCRUE(tc_plain_transform); // (this is also called directly instead of through ogl_tex, so // we need to validate) CHECK_TEX(t); // extract texture info const uint w = t->w, h = t->h, bpp = t->bpp, flags = t->flags; u8* const data = tex_get_data(t); const size_t data_size = tex_img_size(t); // sanity checks (not errors, we just can't handle these cases) // .. unknown transform if(transforms & ~(TEX_BGR|TEX_ORIENTATION|TEX_MIPMAPS)) return INFO::TEX_CODEC_CANNOT_HANDLE; // .. data is not in "plain" format RETURN_ERR(tex_validate_plain_format(bpp, flags)); // .. nothing to do if(!transforms) return INFO::OK; // allocate copy of the image data. // rationale: L1 cache is typically A2 => swapping in-place with a // line buffer leads to thrashing. we'll assume the whole texture*2 // fits in cache, allocate a copy, and transfer directly from there. // // this is necessary even when not flipping because the initial data // is read-only. shared_ptr newData = io_Allocate(data_size); cpu_memcpy(newData.get(), data, data_size); // setup row source/destination pointers (simplifies outer loop) u8* dst = (u8*)newData.get(); const u8* src = (const u8*)newData.get(); const size_t pitch = w * bpp/8; // .. avoid y*pitch multiply in row loop; instead, add row_ofs. ssize_t row_ofs = (ssize_t)pitch; // flipping rows (0,1,2 -> 2,1,0) if(transforms & TEX_ORIENTATION) { src = (const u8*)data+data_size-pitch; // last row row_ofs = -(ssize_t)pitch; } // no BGR convert necessary if(!(transforms & TEX_BGR)) { for(uint y = 0; y < h; y++) { cpu_memcpy(dst, src, pitch); dst += pitch; src += row_ofs; } } // RGB <-> BGR else if(bpp == 24) { for(uint y = 0; y < h; y++) { for(uint x = 0; x < w; x++) { // need temporaries in case src == dst (i.e. not flipping) const u8 b = src[0], g = src[1], r = src[2]; dst[0] = r; dst[1] = g; dst[2] = b; dst += 3; src += 3; } src += row_ofs - pitch; // flip? previous row : stay } } // RGBA <-> BGRA else if(bpp == 32) { for(uint y = 0; y < h; y++) { for(uint x = 0; x < w; x++) { // need temporaries in case src == dst (i.e. not flipping) const u8 b = src[0], g = src[1], r = src[2], a = src[3]; dst[0] = r; dst[1] = g; dst[2] = b; dst[3] = a; dst += 4; src += 4; } src += row_ofs - pitch; // flip? previous row : stay } } t->data = newData; t->dataSize = data_size; t->ofs = 0; if(!(t->flags & TEX_MIPMAPS) && transforms & TEX_MIPMAPS) RETURN_ERR(add_mipmaps(t, w, h, bpp, newData.get(), data_size)); CHECK_TEX(t); return INFO::OK; } TIMER_ADD_CLIENT(tc_transform); // change 's pixel format by flipping the state of all TEX_* flags // that are set in transforms. LibError tex_transform(Tex* t, uint transforms) { TIMER_ACCRUE(tc_transform); CHECK_TEX(t); const uint target_flags = t->flags ^ transforms; uint remaining_transforms; for(;;) { remaining_transforms = target_flags ^ t->flags; // we're finished (all required transforms have been done) if(remaining_transforms == 0) return INFO::OK; LibError ret = tex_codec_transform(t, remaining_transforms); if(ret != INFO::OK) break; } // last chance RETURN_ERR(plain_transform(t, remaining_transforms)); return INFO::OK; } // change 's pixel format to the new format specified by . // (note: this is equivalent to tex_transform(t, t->flags^new_flags). LibError tex_transform_to(Tex* t, uint new_flags) { // tex_transform takes care of validating const uint transforms = t->flags ^ new_flags; return tex_transform(t, transforms); } //----------------------------------------------------------------------------- // image orientation //----------------------------------------------------------------------------- // see "Default Orientation" in docs. static int global_orientation = TEX_TOP_DOWN; // set the orientation (either TEX_BOTTOM_UP or TEX_TOP_DOWN) to which // all loaded images will automatically be converted // (excepting file formats that don't specify their orientation, i.e. DDS). void tex_set_global_orientation(int o) { debug_assert(o == TEX_TOP_DOWN || o == TEX_BOTTOM_UP); global_orientation = o; } static void flip_to_global_orientation(Tex* t) { // (can't use normal CHECK_TEX due to void return) WARN_ERR(tex_validate(t)); uint orientation = t->flags & TEX_ORIENTATION; // if codec knows which way around the image is (i.e. not DDS): if(orientation) { // flip image if necessary uint transforms = orientation ^ global_orientation; WARN_ERR(plain_transform(t, transforms)); } // indicate image is at global orientation. this is still done even // if the codec doesn't know: the default orientation should be chosen // to make that work correctly (see "Default Orientation" in docs). t->flags = (t->flags & ~TEX_ORIENTATION) | global_orientation; // (can't use normal CHECK_TEX due to void return) WARN_ERR(tex_validate(t)); } // indicate if the orientation specified by matches // dst_orientation (if the latter is 0, then the global_orientation). // (we ask for src_flags instead of src_orientation so callers don't // have to mask off TEX_ORIENTATION) bool tex_orientations_match(uint src_flags, uint dst_orientation) { const uint src_orientation = src_flags & TEX_ORIENTATION; if(dst_orientation == 0) dst_orientation = global_orientation; return (src_orientation == dst_orientation); } //----------------------------------------------------------------------------- // misc. API //----------------------------------------------------------------------------- // indicate if 's extension is that of a texture format // supported by tex_load. case-insensitive. // // rationale: tex_load complains if the given file is of an // unsupported type. this API allows users to preempt that warning // (by checking the filename themselves), and also provides for e.g. // enumerating only images in a file picker. // an alternative might be a flag to suppress warning about invalid files, // but this is open to misuse. bool tex_is_known_extension(const char* filename) { const TexCodecVTbl* dummy; // found codec for it => known extension const std::string extension = fs::extension(filename); if(tex_codec_for_filename(extension, &dummy) == INFO::OK) return true; return false; } // store the given image data into a Tex object; this will be as if // it had been loaded via tex_load. // // rationale: support for in-memory images is necessary for // emulation of glCompressedTexImage2D and useful overall. // however, we don't want to provide an alternate interface for each API; // these would have to be changed whenever fields are added to Tex. // instead, provide one entry point for specifying images. // // we need only add bookkeeping information and "wrap" it in // our Tex struct, hence the name. LibError tex_wrap(uint w, uint h, uint bpp, uint flags, shared_ptr data, size_t ofs, Tex* t) { t->w = w; t->h = h; t->bpp = bpp; t->flags = flags; t->data = data; t->dataSize = ofs + w*h*bpp/8; t->ofs = ofs; CHECK_TEX(t); return INFO::OK; } // free all resources associated with the image and make further // use of it impossible. void tex_free(Tex* t) { // do not validate - this is called from tex_load if loading // failed, so not all fields may be valid. t->data.reset(); // do not zero out the fields! that could lead to trouble since // ogl_tex_upload followed by ogl_tex_free is legit, but would // cause OglTex_validate to fail (since its Tex.w is == 0). } //----------------------------------------------------------------------------- // getters //----------------------------------------------------------------------------- // returns a pointer to the image data (pixels), taking into account any // header(s) that may come before it. u8* tex_get_data(const Tex* t) { // (can't use normal CHECK_TEX due to u8* return value) WARN_ERR(tex_validate(t)); u8* p = t->data.get(); if(!p) return 0; return p + t->ofs; } static void add_level_size(uint UNUSED(level), uint UNUSED(level_w), uint UNUSED(level_h), const u8* RESTRICT UNUSED(level_data), size_t level_data_size, void* RESTRICT cbData) { size_t* ptotal_size = (size_t*)cbData; *ptotal_size += level_data_size; } // return total byte size of the image pixels. (including mipmaps!) // this is preferable to calculating manually because it's // less error-prone (e.g. confusing bits_per_pixel with bytes). size_t tex_img_size(const Tex* t) { // (can't use normal CHECK_TEX due to size_t return value) WARN_ERR(tex_validate(t)); const int levels_to_skip = (t->flags & TEX_MIPMAPS)? 0 : TEX_BASE_LEVEL_ONLY; const uint data_padding = (t->flags & TEX_DXT)? 4 : 1; size_t out_size = 0; tex_util_foreach_mipmap(t->w, t->h, t->bpp, 0, levels_to_skip, data_padding, add_level_size, &out_size); return out_size; } // return the minimum header size (i.e. offset to pixel data) of the // file format indicated by 's extension (that is all it need contain: // e.g. ".bmp"). returns 0 on error (i.e. no codec found). // this can be used to optimize calls to tex_write: when allocating the // buffer that will hold the image, allocate this much extra and // pass the pointer as base+hdr_size. this allows writing the header // directly into the output buffer and makes for zero-copy IO. size_t tex_hdr_size(const char* fn) { const TexCodecVTbl* c; const std::string extension = fs::extension(fn); CHECK_ERR(tex_codec_for_filename(extension, &c)); return c->hdr_size(0); } //----------------------------------------------------------------------------- // read/write from memory and disk //----------------------------------------------------------------------------- LibError tex_decode(shared_ptr data, size_t data_size, Tex* t) { const TexCodecVTbl* c; RETURN_ERR(tex_codec_for_header(data.get(), data_size, &c)); // make sure the entire header is available const size_t min_hdr_size = c->hdr_size(0); if(data_size < min_hdr_size) WARN_RETURN(ERR::TEX_INCOMPLETE_HEADER); const size_t hdr_size = c->hdr_size(data.get()); if(data_size < hdr_size) WARN_RETURN(ERR::TEX_INCOMPLETE_HEADER); t->data = data; t->dataSize = data_size; t->ofs = hdr_size; // for orthogonality, encode and decode both receive the memory as a // DynArray. package data into one and free it again after decoding: DynArray da; RETURN_ERR(da_wrap_fixed(&da, data.get(), data_size)); RETURN_ERR(c->decode(&da, t)); // note: not reached if decode fails. that's not a problem; // this call just zeroes and could be left out. (void)da_free(&da); // sanity checks if(!t->w || !t->h || t->bpp > 32) WARN_RETURN(ERR::TEX_FMT_INVALID); if(t->dataSize < t->ofs + tex_img_size(t)) WARN_RETURN(ERR::TEX_INVALID_SIZE); flip_to_global_orientation(t); CHECK_TEX(t); return INFO::OK; } LibError tex_encode(Tex* t, const std::string& extension, DynArray* da) { CHECK_TEX(t); CHECK_ERR(tex_validate_plain_format(t->bpp, t->flags)); // we could be clever here and avoid the extra alloc if our current // memory block ensued from the same kind of texture file. this is // most likely the case if in_img == tex_get_data() + c->hdr_size(0). // this would make for zero-copy IO. const size_t max_out_size = tex_img_size(t)*4 + 256*KiB; RETURN_ERR(da_alloc(da, max_out_size)); const TexCodecVTbl* c; CHECK_ERR(tex_codec_for_filename(extension, &c)); // encode into LibError err = c->encode(t, da); if(err < 0) { (void)da_free(da); WARN_RETURN(err); } return INFO::OK; }