0ad/source/lib/tex/tex.cpp
janwas c0ed950657 had to remove uint and ulong from lib/types.h due to conflict with other library.
this snowballed into a massive search+destroy of the hodgepodge of
mostly equivalent types we had in use (int, uint, unsigned, unsigned
int, i32, u32, ulong, uintN).

it is more efficient to use 64-bit types in 64-bit mode, so the
preferred default is size_t (for anything remotely resembling a size or
index). tile coordinates are ssize_t to allow more efficient conversion
to/from floating point. flags are int because we almost never need more
than 15 distinct bits, bit test/set is not slower and int is fastest to
type. finally, some data that is pretty much directly passed to OpenGL
is now typed accordingly.

after several hours, the code now requires fewer casts and less
guesswork.

other changes:
- unit and player IDs now have an "invalid id" constant in the
respective class to avoid casting and -1
- fix some endian/64-bit bugs in the map (un)packing. added a
convenience function to write/read a size_t.
- ia32: change CPUID interface to allow passing in ecx (required for
cache topology detection, which I need at work). remove some unneeded
functions from asm, replace with intrinsics where possible.

This was SVN commit r5942.
2008-05-11 18:48:32 +00:00

658 lines
20 KiB
C++

/**
* =========================================================================
* 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 <math.h>
#include <stdlib.h>
#include <algorithm>
#include "lib/timer.h"
#include "lib/bits.h"
#include "lib/sysdep/cpu.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 size_t dxt = t->flags & TEX_DXT;
if(dxt != 0 && dxt != 1 && dxt != DXT1A && dxt != 3 && dxt != 5)
WARN_RETURN(ERR::_4);
// .. orientation
const size_t 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(size_t bpp, int 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(size_t w, size_t h, size_t bpp, const u8* pixels, int levels_to_skip, size_t data_padding, MipmapCB cb, void* RESTRICT cbData)
{
debug_assert(levels_to_skip >= 0 || levels_to_skip == TEX_BASE_LEVEL_ONLY);
size_t 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 <data>
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((size_t)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
{
size_t num_components;
size_t prev_level_w;
size_t prev_level_h;
const u8* prev_level_data;
size_t prev_level_data_size;
};
// uses 2x2 box filter
static void create_level(size_t level, size_t level_w, size_t 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 size_t 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(size_t y = 0; y < std::max(src_w, src_h); y += 2)
{
for(size_t 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(size_t y = 0; y < src_h; y += 2)
{
for(size_t x = 0; x < src_w; x += 2)
{
for(size_t 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, size_t w, size_t h, size_t 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<u8> 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, size_t 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 size_t w = t->w, h = t->h, bpp = t->bpp;
const int 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<u8> 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(size_t y = 0; y < h; y++)
{
cpu_memcpy(dst, src, pitch);
dst += pitch;
src += row_ofs;
}
}
// RGB <-> BGR
else if(bpp == 24)
{
for(size_t y = 0; y < h; y++)
{
for(size_t 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(size_t y = 0; y < h; y++)
{
for(size_t 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 <t>'s pixel format by flipping the state of all TEX_* flags
// that are set in transforms.
LibError tex_transform(Tex* t, size_t transforms)
{
TIMER_ACCRUE(tc_transform);
CHECK_TEX(t);
const size_t target_flags = t->flags ^ transforms;
size_t 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 <t>'s pixel format to the new format specified by <new_flags>.
// (note: this is equivalent to tex_transform(t, t->flags^new_flags).
LibError tex_transform_to(Tex* t, size_t new_flags)
{
// tex_transform takes care of validating <t>
const size_t 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));
size_t orientation = t->flags & TEX_ORIENTATION;
// if codec knows which way around the image is (i.e. not DDS):
if(orientation)
{
// flip image if necessary
size_t 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 <src_flags> 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(size_t src_flags, size_t dst_orientation)
{
const size_t src_orientation = src_flags & TEX_ORIENTATION;
if(dst_orientation == 0)
dst_orientation = global_orientation;
return (src_orientation == dst_orientation);
}
//-----------------------------------------------------------------------------
// misc. API
//-----------------------------------------------------------------------------
// indicate if <filename>'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(size_t w, size_t h, size_t bpp, int flags, shared_ptr<u8> 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 <t> - 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(size_t UNUSED(level), size_t UNUSED(level_w), size_t 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 size_t 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 <fn>'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 VfsPath& filename)
{
const TexCodecVTbl* c;
const std::string extension = fs::extension(filename);
CHECK_ERR(tex_codec_for_filename(extension, &c));
return c->hdr_size(0);
}
//-----------------------------------------------------------------------------
// read/write from memory and disk
//-----------------------------------------------------------------------------
LibError tex_decode(shared_ptr<u8> 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 <da> 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 <da>
LibError err = c->encode(t, da);
if(err < 0)
{
(void)da_free(da);
WARN_RETURN(err);
}
return INFO::OK;
}