Deep Texture API

Deep Texture API

August, 2003

  • Important

    * The Deep Texture API has been deprecated in favor of RixDeeptexture.

Introduction

This application note describes the dtex library. This library provides the structures and functions necessary to create, load, and modify Pixar deep texture map files. It maintains a tile cache under the covers, making it possible to work on files without loading them entirely into memory.

The API supports an arbitrary number of images in a single file, each with its own resolution, tile size, and view matrices. It also supports filtering of multiple depth functions and a lossy compression technique.

For more information on deep shadows, see Tom Lokovic, Eric Veach. "Deep Shadow Maps". Computer Graphics (SIGGRAPH 2000 Conference Proceedings), pages 385-392. ACM, July 2000.


Error Codes

Unless otherwise specified, an int return value from a library return is an error code. These error codes are:

  • DTEX_TILECOMPRESSION: Error during tile compression
  • DTEX_UNFINISHED: Illegal operation on pixel with unresolved compression state
  • DTEX_RANGE: Out of range parameter
  • DTEX_NOMEM: Ran out of memory
  • DTEX_EMPTY: Illegal operation on an empty structure
  • DTEX_NOIMAGE: The specified image was not found
  • DTEX_UNSUPPORTED: The specified operation is not supported
  • DTEX_BADFILE: File was corrupt
  • DTEX_NOFILE: File was not found or was invalid
  • DTEX_ERR: Miscellaneous error occurred
  • DTEX_NOERR: No error

Cache Management

Before working with deep shadow map files a tile cache must be created. The tile cache makes it possible to work on deep shadow files without loading them entirely into memory.

Once the tile cache has been created, pixel access routines will transparently use the tile cache. Note that, when outputting deep shadow files, it is important to access pixels in a coherent fashion, as thrashing the tile cache will result in nonoptimal deep shadow files. In other words, ideally, once a tile leaves the cache (and is flushed to disk) there should be no need to revisit this tile in the future.

For example, if you plan to write deep shadow map pixels in scanline order, make sure that the tile cache is large enough to contain one scanline's worth of tiles. If you are rendering an image that is 1024 pixels wide and are using a tilesize of 16 pixels, you will need to construct a tile cache containing at least 64 tiles. As each scanline is completed tiles will leave the cache and be flushed to disk, and will not need to be reloaded into the cache.

DtexCache * DtexCreateCache(int numTiles, DtexAccessor *accessor);
int DtexDestroyCache(DtexCache *c);
int DtexSyncCache(DtexCache *dsc);

These routines create, destroy, and synchronize tile caches. accessor specifies a set of I/O functions to use upon faults. If accessor is NULL, standard system calls are used.

The DtexSyncCache routine writes out all modified tiles of all files in the cache, causing the files on disk to reflect any changes that have been made.

DtexCreateCache returns NULL if no cache was created.


File Management

The following functions are used to create new deep texture files, or to open existing deep texture files. A cache must be created before using these routines.

int DtexOpenFile(const char *name, const char *mode, DtexCache *cache, DtexFile **result);
int DtexCreateFile(const char *name, DtexCache *cache, DtexFile **result);
int DtexClose(DtexFile *ds);
int DtexSync(DtexFile *ds);

These routines open, create, close, and synchronize deep texture files.

Calling either DtexClose or DtexSync on a texture file will write out all modified tiles, causing the file on disk to reflect any changes that have been made.


Image Management

These routines manage the deep texture images within a given file. Currently, Pixar's RenderMan only recognizes the first deep texture image as a deep shadow map, but a future release may support arbitrary numbers of images.

int DtexAddImage(DtexFile *f, const char *name, int numChan,
                 int width, int height, int tilewidth, int tileheight,
                 float *NP, float *Nl,
                 enum DtexCompression compression,
                 enum DtexDataType datatype,
                 DtexImage **result);
int DtexCountImages(DtexFile *f);
int DtexGetImageByName(DtexFile *f, const char *name, DtexImage **result);
int DtexGetImageByIndex(DtexFile *f, int index, DtexImage **result);

DtexAddImage constructs a new image with the given dimensions and adds it to the given file, returning a pointer to a new DtexImage.

DtexCountImages returns the number of images in the given file.

DtexGetImageByName and DtexGetImageByIndex return a pointer to the an image with the given name or index. They will return DTEX_NOIMAGE if the given image doesn't exist. The resulting pointer is guaranteed to be valid until DtexClose() is called on the containing file.

int DtexWidth(DtexImage *i);
char *DtexImageName(DtexImage *i);
int DtexNumChan(DtexImage *i);
int DtexHeight(DtexImage *i);
int DtexTileWidth(DtexImage *i);
int DtexTileHeight(DtexImage *i);
int DtexNP(DtexImage *i, float *NP);
int DtexNl(DtexImage *i, float *Nl);
DtexCompression DtexGetCompression(DtexImage *i);
DtexDataType DtexGetDataType(DtexImage *i);
These routines return the appropriate information from the given image.
int DtexSetPixelData(DtexImage *img, int x, int y, int numChan, int numPoints,
                     float *data);
Copies the given pixel data into pixel x,y of the specified image. numChan indicates how many channels of data are being provided, and numPoints indicates how many points there are. data should point to numPoints*(numChan+1) floats.
int DtexSetPixel(DtexImage *img, int x, int y, DtexPixel *pix);
Copies the given pixel into pixel x,y of the specified image. pix must have either the same number of channels as img, or one channel. If pix has any unresolved compression state (i.e. DtexFinishPixel was not called before using DtexSetPixel), DTEX_ERR is returned.
int DtexGetPixel(DtexImage *img, int x, int y, DtexPixel *pix);
Copies the pixel at position x,y into the given pixel structure. The pixel structure is resized if necessary, and if the number of channels in pix is different than the number of channels in img, pix is modified to match. Returns DTEX_NOERR if no error, DTEX_RANGE if the requested pixel is outside the image, or DTEX_BADFILE if the file is found to be corrupt.
int DtexEval(DtexImage *img, int x, int y, float z, int n, float *data);
Looks up the pixel's interpolated value at depth z, and puts the result in data. n must indicate how many floats are allocated in data, and must match the number of channels in the image.
int DtexGetZRange(DtexImage *img, int x, int y, float *min, float *max);
Computes the range over which the specified pixel is defined. If the pixel is non-empty, the range is returned in min and max and the function returns DTEX_NOERR. If the pixel is empty, DTEX_EMPTY is returned and min and max are undefined.
int DtexGetMeanDepth(DtexImage *img, int x, int y, float *mean, float *alpha);
Computes the mean depth found in the pixel and the overall coverage. This is useful for displaying deep images.

Pixel Management

Because deep texture pixels vary in size, a pixel's storage must be dynamically allocated. This library provides a type, DtexPixel, which allows users to build and evaluate pixels. The structure is fairly heavyweight because it stores auxiliary information related to compression. We don't recommend allocating an entire image of these structures yourself; keep a small number of DtexPixel's around, and use DtexSetPixel to modify a DtexImage.

The following functions let the user create, modify, and destroy DtexPixel's. Pixels may be cleared with DtexClearPixel(). New datapoints may be added (in increasing Z order) with DtexAppendPixel(). If compression is used, a pixel must be DtexFinish()'ed before lookups can be performed in the pixel.

DtexPixel * DtexMakePixel(int numChan);
void DtexDestroyPixel(DtexPixel *pix);
DtexMakePixel creates a DtexPixel structure which should be destroyed with DtexDestroyPixel, not free().
int DtexClearPixel(DtexPixel *pix, int numChan);
int DtexEmptyPixel(DtexPixel *pix);
DtexClearPixel marks the specified DtexPixel as "empty", but does not affect its allocation. To destroy the allocation as well, use DtexEmptyPixel; note that this does not free the DtexPixel itself.
int DtexSpecifyPixel(DtexPixel *pix,int numChan,int numPoints, float *data);
Sets the given pixel to have numChan channels and sets its data directly. Assumes no compression.
int DtexIsPixelMonochrome(DtexPixel *p);
int DtexPixelGetNumChan(DtexPixel *pix);
int DtexPixelGetNumPoints(DtexPixel *pix);
DtexIsPixelMonochrome returns 1 if the specific pixel has duplicates for all its values, zero otherwise. If the pixel has no control points, returns 1. DtexPixelGetNumChan returns the number of channels in the pixel. DtexPixelGetNumPoints returns the number of control points in the pixel. Returns zero if there are no control points, -1 if the point has unresolved compression state.
int DtexPixelGetPoint(DtexPixel *pix, int i, float *z, float *data);
Gets the i'th control point in the given pixel. If the indicated control point is accessible, z and data are set, and DTEX_NOERR is returned. If the pixel does not exist, DTEX_NOPOINT is returned and *z and *data are undefined. If the pixel has unresolved compression state, DTEX_UNFINISHED is returned and *z and *data are undefined.
int DtexPixelSetPoint(DtexPixel *pix, int i, float z, float *data);
Modifies the i'th control point in the given pixel. If the indicated control point is accessible, and if the supplied z is legal, the pixel is modified and DTEX_NOERR is returned. If the pixel does not exist, DTEX_NOPOINT is returned. If the pixel has unresolved compression state, DTEX_UNFINISHED is returned and *z and *data are undefined.
int DtexCopyPixel(DtexPixel *dest, DtexPixel *src);
Copies src to dest. If dest has a different number of channels than src, dest is modified to match. This includes current compression state.
int DtexMergePixel(DtexPixel *dest, DtexPixel *src);
Merges data from src to dest - the resulting data in dest will have all control points that were present in both pixels and be sorted by z-order. Both pixels must have the same number of channels, and must not have unresolved compression state.
int DtexMergePixelEx(DtexPixel *dest, DtexPixel *src, int rgbChannel, int alphaChannel);

Like DtexMergePixel, this merges data from src into dest. In addition, it can correctly handle embedding geometry in volumes and overlapping volumes. These volume segments are flagged with a negative alpha and extend to the next sample in the pixel. There must be at least one non-volume sample marking the end of chain (which may be completely transparent).

This assumes a four channel RGBA pixel function when splitting and merging volume regions. The rgbChannel must be the index of the first color channel, with the assumption that all three color channels are contiguous. The alphaChannel must be the index of the alpha channel. Any additional channels will be copied unchanged from the most recent volume sample.

int DtexFinishPixel(DtexPixel *dest);
If the specified pixel is in "compress mode" (non-zero compression), this function finishes compression and sets compression error to zero. This must be called before any lookups on the pixel structure. If the pixel already has a zero compression error, this function has no effect.
int DtexAppendPixel(DtexPixel *pix, float z, int n, float *data, float error);

Adds a data point to the end of the specified pixel. n must specify how many floats are stored in data, and must match the number of channels in the pixel.

If error is non-zero, a lossy compression technique is applied. This error parameter corresponds to the error tolerance ε described in section 3.3 of "Deep Shadow Maps" by Lokovic and Veach. To clarify how this compression works using this API:

  • The first call to DtexAppendPixel always adds the pixel to the depth function. Call this point A.
  • The next call to DtexAppendPixel results in a candidate pixel, which is not immediately added. Call this point B.
  • The next call to DtexAppendPixel with a point C checks whether the previous candidate pixel B can be discarded based on whether the target window drawn from A to B intersects C. The target window itself can be imagined by considering B_hi and B_lo, where B_hi is some distance directly above B and B_low is directly below B. The tangent window is the area between A to B_hi and A to B_lo, and the distance between B_hi and B_lo is directly related to the error parameter. If the new pixel (C) is inside the target window then the candidate (B) is discarded. Otherwise, the candidate (B) is added to the list of pixels.
  • The new point becomes the new candidate pixel.
int DtexEvalPixel(DtexPixel *pix, float z, int n, float *data);
Evaluates the pixel's function at depth z, and puts the result in data. n must indicate how many floats are allocated in data, and must match the number of channels in the image.
int DtexCompositePixel(DtexPixel *pix, int rgbChannel, int alphaChannel, float *data);

Composite a four channel RGBA pixel function and store the result in data. rgbChannel must be the index of the first color channel, with the assumption that all three color channels are contiguous. alphaChannel must be the index of the alpha channel. data must be a buffer of at least four floats.

If the pixel is empty, DTEX_EMPTY is returned and data is filled with zeros. Otherwise, data[0], data[1], and data[2] contain the composited color values and data[3] contains the composited alpha value, and DTEX_NOERR is returned.

int DtexGetPixelZRange(DtexPixel *pix, float *min, float *max);
Computes the range over which the specified pixel is defined. If the pixel is non-empty, the range is returned in min and max and the function returns DTEX_NOERR. If the pixel is empty, DTEX_EMPTY is returned and min and max are undefined. If the pixel has unresolved compression state, DTEX_UNFINISHED is returned and min and max are undefined.
int DtexPrintPixel(DtexPixel *p);
Prints a representation of the given pixel to stdout.
int DtexAveragePixels(int n, DtexPixel **pixels, float *weights, float error, DtexPixel *result);
Computes the weighted pointwise average of the given n pixels and puts the result in result. The interpretation of error is the same as in DtexAppendPixel, i.e. it controls lossy compression.

Filtering

These routines provide functionality for the filtering of depth functions (i.e. the filtering of subpixel depth functions into a single depth function for the pixel). Filtering works by creating a DtexDeepFilter structure, filling in function information, then telling it to compute the result.

For efficiency, the DtexDeepFilter structure requires function information as a list of deltas rather than points. Specifically, we assume the function starts out at z=0 with a value of 1, followed by a list of deltas to the slope or position of the function.

Here's how a user would filter n deep functions:

  • Call DtexCreateDeepFilter() to create a filter structure.
  • For each input function, we have to compute the following:
    • Its filter weight.
    • How many deltas are needed to represent it.
  • Put this information in two n-sized arrays.
  • Pass this information to DtexGetDeepFilterData(). This sets up the function based on your information and returns a delta buffer.
  • Run through your input functions again, in the same order, and put the delta data into the delta buffer. You must put exactly the right number of deltas to match the number of deltas you specified earlier.
  • Call DtexComputeDeepPointData() to get the filtered result.
DtexDeepFilter *DtexCreateDeepFilter(void);
void DtexDestroyDeepFilter(DtexDeepFilter *filter);
Creates and destroys a deep filter.
float *DtexGetDeepFilterData(DtexDeepFilter *filter, int numChan, int numSamples, int *numDeltas,
                             float *filterWeights, int totalNumDeltas);
Prepares the given filter to take samples. numChan is the number of channels (in addition to z) per delta. numSamples is the number of samples in the filter. numDeltas is an array with numSamples values indicating, for each sample, how many deltas are in that sample. filterWeights is an array with numSamples floats indicating, for each sample, what that sample's filterweight is. totalNumDeltas is the sum of the values in numDeltas. When this function is done, all the samples will be set up. We return a pointer to the delta data so the user can then run through and fill in the actual deltas.
int DtexComputeDeepPointData(DtexDeepFilter *filter, float *pointData, float error, int assumeSmooth);

Executes the given filter to produce point data. pointData must contain enough space to hold "totalNumDeltas" deltas as specified in DtexGetDeepFilterData.

If assumeSmooth is non-zero, the computation will assume that the underlying function is smooth, and that any discontinuities encountered are part of the sampling error. When we filter several functions to produce a new function, the input functions usually have discontinuities. By definition, the output function will thus have discontinuities. But in some cases, the true filtered function would have no discontinuties. "assumeSmooth" says we assume that the function should be smooth, so we ignore the discontinuties by dropping the top point of each one. This dropping happens after the functions have been filtered, but before the compression.

The interpretation of error is the same as in DtexAppendPixel, i.e. it controls lossy compression. When filtering across each point the same lossy compression technique is considered in turn.

int DtexCompressPointData(float *pointData, int numChan, int numPoints, float *result, float error);
Given a series of points, compresses it using the lossy technique described in DtexAppendPixel. The error parameter controls the extent of compression.
int DtexCompressPixel(DtexPixel *src, DtexPixel *dest, float error);
Compresses one pixel and puts the result in another using the lossy technique described in DtexAppendPixel. The error parameter controls the extent of compression. This function does not work in place (i.e. dest must not == src).

Utility Routines

void DtexQueryMemory(long *current,long *peak);
Returns the current and peak memory usage of the dtex library system.

Example

The following code demonstrates how to use the dtex library to read deep shadow files, read and manipulate pixels, and create new deep shadow files. When built, the dsmerge program allows the user to merge deep shadow map files of the same dimension into new deep shadow map files. Depth functions at each pixel are combined together in sorted order. It may be invoked with:

dsmerge infile1.dshd infile2.dshd  ... outfile.dshd
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <dtex.h>
#include <assert.h>

int
main(int argc, char *argv[]) {
    float NP[16], Nl[16];
    int i, j, x, y;
    DtexCache *inCache, *outCache;
    DtexImage **inImages, *outImage;
    float *inData, *outData;
    float iz, oz;
    int width, height, nChannels;
    int nInFiles;
    int nInPoints, nOutPoints;
    int inIndex, outIndex;
    char *inname, *outname;
    DtexFile **inFiles, *outFile;
    DtexPixel *inPixel, *outPixel, *mergedPixel;

    if (argc < 3) {
        fprintf(stderr, "Must supply at least one input file and an output.\n");
        exit(1);
    }

    /*
     * Create the tile cache for the output image. In this
     * application, this needs to be large enough to hold one
     * scanline's worth of tiles, due to our scanline, perpixel
     * pattern of access.
     */
    outCache = DtexCreateCache(128, NULL);
    /*
     * Construct a separate tile cache for input images.  Again, it
     * should be large enough to hold one scanline's worth of tiles,
     * but from each input image. The choice of cache size here will
     * affect only speed, since if the cache thrashes we just end up
     * rereading the tile from the input image.
     */
    inCache = DtexCreateCache(256, NULL);

    if (!outCache) {
        fprintf(stderr, "Unable to create output tile cache.\n");
        exit(1);
    }

    /* Create output file */
    outname = argv[argc - 1];
    if (DtexCreateFile(outname, outCache, &amp;outFile) != DTEX_NOERR) {
        fprintf(stderr, "Unable to open output file %s.\n", outname);
        exit(1);
    }

    nInFiles = 0;
    inImages = (DtexImage**) malloc((argc - 1) * sizeof(DtexImage*));
    inFiles = (DtexFile**) malloc((argc - 1) * sizeof(DtexFile*));
    if (!inImages || !inFiles) {
        fprintf(stderr, "Unable to open allocate memory for input files.\n");
        exit(1);
    }

    /*
     * Loop over the input files, checking for validity and opening
     * the first input image
     */
    for (i = 1; i < argc - 1; ++i) {
        inname = argv[i];
        if (DtexOpenFile(inname, "rb", inCache, &amp;inFiles[nInFiles]) != DTEX_NOERR) {
            fprintf(stderr, "Unable to open input file %s, skipping it.\n", inname);
            continue;
        }
        if (DtexCountImages(inFiles[nInFiles]) != 1) {
            fprintf(stderr, "Input file %s has number of images != 1, skipping.\n", inname);
            DtexClose(inFiles[nInFiles]);
            continue;
        }

        if (DtexGetImageByIndex(inFiles[nInFiles], 0, &amp;inImages[nInFiles]) != DTEX_NOERR) {
            fprintf(stderr, "Unable to access image zero in input file %s, skipping.\n", inname);
            DtexClose(inFiles[nInFiles]);
            continue;
        }

        if (nInFiles == 0) {
            /* Create the appropriate output image now */
            DtexNP(inImages[0], NP);
            DtexNl(inImages[0], Nl);
            if (DtexAddImage(outFile,
                             DtexImageName(inImages[0]),
                             DtexNumChan(inImages[0]),
                             DtexWidth(inImages[0]),
                             DtexHeight(inImages[0]),
                             DtexTileWidth(inImages[0]),
                             DtexTileHeight(inImages[0]),
                             NP,
                             Nl,
                             DtexGetCompression(inImages[0]),
                             DtexGetDataType(inImages[0]),
                             &outImage) != DTEX_NOERR) {
                fprintf(stderr, "Unable to create output image %s.\n", outname);
                exit(1);
            }
        }

        /*
         * Check that the output and input images have the
         * same dimensionality. At the moment this is limited
         * to number of channels, width, height, and data
         * type.
         */
        else if (DtexNumChan(inImages[nInFiles]) != DtexNumChan(outImage) ||
            DtexWidth(inImages[nInFiles]) != DtexWidth(outImage) ||
            DtexHeight(inImages[nInFiles]) != DtexHeight(outImage) ||
            DtexGetDataType(inImages[nInFiles]) != DtexGetDataType(outImage)) {
            fprintf(stderr, "Input file %s does not have matching dimensions as output image, skipping.\n", inname);
            DtexClose(inFiles[nInFiles]);
            continue;
        }

        nInFiles++;
    }

    /* Set up invariants over the set of input images */
    width = DtexWidth(inImages[0]);
    height = DtexHeight(inImages[0]);
    nChannels = DtexNumChan(inImages[0]);
    inData = (float*) malloc(nChannels * 2 * sizeof(float));
    outData = inData + nChannels;
    inPixel = DtexMakePixel(nChannels);
    outPixel = DtexMakePixel(nChannels);
    mergedPixel = DtexMakePixel(nChannels);
    if (!inPixel || !outPixel || !mergedPixel) {
        fprintf(stderr, "Unable to create pixels\n");
        exit(1);
    }

    /* Loop over the scanlines in the image and merge pixels */
    for (y = 0; y < height; ++y) {
        for (x = 0; x < width; ++x) {
            for (i = 0; i < nInFiles; ++i) {

                DtexClearPixel(inPixel, nChannels);
                DtexClearPixel(outPixel, nChannels);
                DtexClearPixel(mergedPixel, nChannels);
                if (DtexGetPixel(inImages[i], x, y, inPixel) != DTEX_NOERR) {
                    fprintf(stderr, "Unable to get input pixel at %d %d from %s.\n", x, y, inname);
                    continue;
                }
                if (DtexGetPixel(outImage, x, y, outPixel) != DTEX_NOERR) {
                    fprintf(stderr, "Unable to get output pixel at %d %d.\n", x, y);
                    continue;
                }

                /* Merge the depth functions */
                nInPoints = DtexPixelGetNumPoints(inPixel);
                nOutPoints = DtexPixelGetNumPoints(outPixel);

                inIndex = outIndex = 0;
                while (1) {

                    /*
                     * If we've finished with the in points,
                     * finish off the merged pixels with the rest
                     * of the out points
                     */
                    if (inIndex == nInPoints) {
                        while (outIndex < nOutPoints) {
                            if (DtexPixelGetPoint(outPixel, outIndex, &amp;oz, outData) != DTEX_NOERR) {
                                fprintf(stderr, "Unable to get output pixel at %d %d\n", x, y);
                                goto nextpixel;
                            }
                            if (DtexAppendPixel(mergedPixel, oz, nChannels, outData, 0) != DTEX_NOERR) {
                                fprintf(stderr, "Unable to append to merged pixel at %d %d\n", x, y);
                                goto nextpixel;
                            }
                            outIndex++;
                        }
                        break;
                    }
                    /*
                     * Likewise, if we've finished the out points,
                     * finish off with the rest of the in points.
                     */
                    if (outIndex == nOutPoints) {
                        while (inIndex < nInPoints) {
                            if (DtexPixelGetPoint(inPixel, inIndex, &amp;iz, inData) != DTEX_NOERR) {
                                fprintf(stderr, "Unable to get input pixel at %d %d\n", x, y);
                                goto nextpixel;

                            }
                            if (DtexAppendPixel(mergedPixel, iz, nChannels, inData, 0) != DTEX_NOERR) {
                                fprintf(stderr, "Unable to append to merged pixel at %d %d\n", x, y);
                                goto nextpixel;
                            }
                            inIndex++;
                        }
                        break;
                    }
                    /* Otherwise just insert in sorted order */
                    if (DtexPixelGetPoint(inPixel, inIndex, &amp;iz, inData) != DTEX_NOERR) {
                        fprintf(stderr, "Unable to get input pixel at %d %d\n", x, y);
                        goto nextpixel;
                    }
                    if (DtexPixelGetPoint(outPixel, outIndex, &amp;oz, outData) != DTEX_NOERR) {
                        fprintf(stderr, "Unable to get output pixel at %d %d\n", x, y);
                        goto nextpixel;
                    }
                    if (iz < oz) {
                        inIndex++;
                        if (DtexAppendPixel(mergedPixel, iz, nChannels, inData, 0) != DTEX_NOERR) {
                            fprintf(stderr, "Unable to append to merged pixel at %d %d\n", x, y);
                            goto nextpixel;
                        }
                    } else {
                        outIndex++;
                        if (DtexAppendPixel(mergedPixel, oz, nChannels, outData, 0) != DTEX_NOERR) {
                            fprintf(stderr, "Unable to append to merged pixel at %d %d\n", x, y);
                            goto nextpixel;
                        }
                    }
                }

                if (DtexFinishPixel(mergedPixel) != DTEX_NOERR) {
                    fprintf(stderr, "Unable to finish output pixel at %d %d\n", x, y);
                    continue;
                }

                if (DtexSetPixel(outImage, x, y, mergedPixel) != DTEX_NOERR) {
                    fprintf(stderr, "Unable to set output pixel at %d %d\n", x, y);
                    continue;
                }

                nextpixel:
                continue;
            }
        }
    }

    DtexDestroyPixel(inPixel);
    DtexDestroyPixel(outPixel);
    DtexDestroyPixel(mergedPixel);

    free(inData);

    for (i = 0; i < nInFiles; ++i) {
        if (DtexClose(inFiles[i]) != DTEX_NOERR) {
            fprintf(stderr, "Unable to close input file %s\n", inname);
        }
    }

    free(inImages);
    free(inFiles);

    if (DtexClose(outFile) != DTEX_NOERR) {
        fprintf(stderr, "Unable to close output file %s\n", outname);
    }

    DtexDestroyCache(inCache);
    DtexDestroyCache(outCache);

    return 0;
}