• entries
    422
  • comments
    1540
  • views
    488794

D3D #21: How to interpret the data returned by a L

Sign in to follow this  
jollyjeffers

214 views

Got another FAQ entry I've just spent the last hour working through. For a change I've not actually added any more "todo" items whilst working on this, so I can genuinely say that I've moved forwards with the FAQ rather than just treading water [lol]

I also got my copy of 'Exceptional C++' by Herb Sutter through the post today. For a mere GBP20 (inc delivery) I figured it'd be silly not to have a read through such a classic text.

I've not read much of it yet, but I already feel stupid. At least some of Scott Meyer's puzzles I could do - Herb Sutter's are just something else [headshake]

Anyway, as usual, comments on the FAQ entry are appreciated:


D3D #21: How to interpret the data returned by a Lock() operation

Please refer to D3D #14: Speeding up locking of resources for more information on locking. The information in this FAQ entry assumes that you've already acquired a lock on a resource.

Most resources can be locked such that Direct3D will return a pointer to the backing data; it is very context-dependent as to quite what this pointer refers to. In the majority of cases this context is known because your code will have created the resource with a known format/description, but in some cases (e.g. when loading unknown external data) it may not be so straight-forward. In these cases you can either try to dynamically interpret the data, or you can provide a number of alternative paths - generating an error if the data doesn't match one of the expected methods.

There are two fundamental operations that you will require - reading and writing. The pointer you will receive will be of unknown (VOID* or VOID**) type so will require you to cast it to a usable type (for readability, a reinterpret_cast is better than C-style casting), even then it may be necessary to decode further. Once you've extracted the required data you can directly manipulate the pointer and have it reflected in the resource. Changes will not be committed until a corresponding unlock operation is made.

In all cases, familiarity with the D3DLOCK and D3DUSAGE enumerations is useful. For example, dereferencing a pointer to data from a resource created with D3DUSAGE_WRITEONLY is A Bad Thing(TM).

Decoding Vertex Buffer Data

Vertex data is typically a multi-element structure, so having a ready-defined struct can be particularly useful. The following code-snippet shows how you might do this:

struct Vertex
{
// appropriate declaration goes here
};

Vertex *pVertexData = NULL; // declare a pointer that we can access data via

pVertexBuffer->Lock( 0, 0, reinterpret_cast< VOID** >( &pVertexData ), 0 );

// Provided Lock() succeeded, we can now access the data:
// pVertexData[0] or (*(pVertexData++)) etc...


In some cases you may wish to decode vertex data that is in an unknown format. A typical example might be when you've loaded geometry from an .X file that has no strict format requirements. Using IDirect3DVertexBuffer9::GetDesc() can be a useful starting point - the D3DVERTEXBUFFER_DESC::FVF can be passed into D3DXGetFVFVertexSize() to determine the stride/size of an individual vertex. The D3DVERTEXBUFFER_DESC::Size field can be used to determine how many vertices exist in the buffer.

This allows you to allocate arbitrary blocks of data that represent a single vertex, and to step through the data per-vertex, but it tells you nothing about the make-up of an individual vertex. The D3DVERTEXBUFFER_DESC::FVF will be useful in determining what data is stored per-vertex, retrieving a declaration via D3DXDeclaratorFromFVF() will be easier to work with. However, it is possible to create an "FVF-less" vertex buffer - one that can't be described by a legacy FVF identifier. If you hit this problem you'll need to find a solution for retrieving the declaration (you can use D3DXGetDeclVertexSize() to get the size of an individual vertex) before continuing. If the vertex buffer belonged to an ID3DXMesh then the ID3DXMesh::GetDeclaration() function is a good starting point.

With the pointer cast to an array of BYTE's (or equivalent) and a valid declaration (D3DVERTEXELEMENT9[]) it is possible to step through the array and use either casting or memcpy_s() operations to extract the individual fields as required.

If an ID3DXMesh is being used as a container for the unknown vertex data then cloning can be a useful trick. ID3DXMesh::CloneMesh() and ID3DXMesh::CloneMeshFVF() can create a temporary clone in a particular format, in particular it can be used to strip out any unwanted data. For example, if only the position element of the vertex is required then calling CloneMeshFVF() with D3DFVF_XYZ will generate a vertex buffer that can be directly cast to a D3DXVECTOR3 (or equivalent).

Decoding Index Buffer Data

Working with data from an index buffer is about as simple as it gets - it will be either 16bit or 32bit unsigned integer form. Using IDirect3DIndexBuffer9::GetDesc() gives you access to D3DINDEXBUFFER_DESC::Format (either D3DFMT_INDEX16 or D3DFMT_INDEX32) and D3DINDEXBUFFER_DESC::Size (which can be used to determine how many indices are in the buffer).

If using Visual C++ the unsigned __int16 and unsigned __int32 data-types (sometimes aliased as UINT16 and UINT32) can be used to address the VOID** that an IDirect3DIndexBuffer9::Lock() call returns:

D3DINDEXBUFFER_DESC desc;
pIndexBuffer->GetDesc( &desc );
if( D3DFMT_INDEX16 == desc.Format )
{
// Access via 16bit integers
unsigned __int16 *pIdx = NULL;

pIndexBuffer->Lock( 0, 0, reinterpret_cast< VOID** >( &pIdx ), 0 );

// We can now manipulate the index data:
// pIdx[...] = ... or (*(pIdx++)) = ...
}
else
{
// Access via 32bit integers
unsigned __int32 *pIdx = NULL;

pIndexBuffer->Lock( 0, 0, reinterpret_cast< VOID** >( &pIdx ), 0 );

// We can now manipulate the index data:
// pIdx[...] = ... or (*(pIdx++)) = ...
}


Even though the above only has 2 branches, the code-duplication can be a potential problem for maintenance and testing. Using C++'s generic programming features can help out here:

template< typename index_format >
ProcessIndexBufferData( IDirect3DIndexBuffer9 *pIB )
{
T* pIdx = NULL;
pIB->Lock( 0, 0, reinterpret_cast< VOID** >( &pIdx ), 0 );

// access as usual
}


The branching is then simplified to:

D3DINDEXBUFFER_DESC desc;
pIndexBuffer->GetDesc( &desc );
if( D3DFMT_INDEX16 == desc.Format )
{
ProcessIndexBufferData< unsigned __int16 >( pIndexBuffer );
}
else
{
ProcessIndexBufferData< unsigned __int32 >( pIndexBuffer );
}


Although, this trick won't work if the processing makes use of any particular 16/32 bit characteristics...

Decoding Texture Data

Texture data is one of the more difficult forms of data to work with; based on the number of threads in forums across the internet it seems to trip up even the best programmers! The following code focuses on regular 2D textures, but the basic principles also apply to cube and volume textures.

Pixel data is usually a hybrid of multiple elements packed into a single value, thus stepping through the pointer returned by IDirect3DTexture9::Lock() is per-pixel, but individual operations might work on only a single part of a pixel. To make matters worse, there are potentially a huge number of texture formats - refer to the D3DFORMAT documentation page for the full list.

There are three important steps:

1. Choose the data-type to cast the pointer to. For a given texture all elements will be of the same format, this format will be of a fixed size (e.g. a 32bit format like D3DFMT_X8R8G8B8). You can therefore cast the VOID* pointer to a known type of the same size. A useful trick can be to cast the data to a struct with the correct layout. For example:

struct Decoded32bitARGB
{
unsigned __int8 A;
unsigned __int8 R;
unsigned __int8 G;
unsigned __int8 B;
};

D3DLOCKED_RECT rect;
pTexture->LockRect( 0, &rect, NULL, 0 );

Decoded32bitARGB *pPixelData = reinterpret_cast< Decoded32bitARGB* >( rect.pBits );

// No need to decode individual channels, just access
// them directly:
pPixelData[..].A = ..;
pPixelData[..].R = ..;
pPixelData[..].G = ..;
pPixelData[..].B = ..;

// An alternative is to cast a 32bit format to a single 32bit integer:

unsigned __int32 *pPixelData = reinterpret_cast< unsigned __int32* >( rect.pBits );

// However, with this method we must manually decode the binary representation of
// 'pPixelData' using bitwise operators...


The struct-based method may not always be possible as the language might not offer the necessary primitives for representing individual channels (e.g. there is no 5 or 6 bit integer type that would be required for D3DFMT_R5G6B5).

2. Step through the array. This might seem obvious, but has a subtle characteristic that can make things explode in your face if you're not careful. IDirect3DTexture9::GetLevelDesc() can allow you to retrieve the pixel dimensions (D3DSURFACE_DESC::Width and D3DSURFACE_DESC::Height), but it's the D3DLOCKED_RECT::Pitch field you must pay attention to. The Direct3D specification allows the hardware to append extra data to the end of each row in a texture - this could either be unused padding (to aid alignment and improve performance) or it may be additional private data required by the device. Thus using a simple array[x][y] or array[x+y*width_in_pixels] lookup might end up addressing incorrect data.

Compensating for the pitch is as easy as remembering to traverse between rows by multiples of D3DLOCKED_RECT::Pitch instead of the number of pixels. Exactly how you do this depends on coding style. Bare in mind that the pitch is measured in bytes, such that you may need to divide it by the size of your per-pixel data.

3. Manipulate individual pixels. Once you've written code for casting and traversing the data you need to be able to read and write the packed data. Not all data (such as D3DFMT_R32F) contains multiple elements, in which case this sub-section can be skipped. Also, if the aforementioned struct-based approach is applicable you may not have to do any low-level manipulation.

Take the 16bit D3DFMT_R5G6B5 for example - if you were to look at the raw 16 binary digits you'd see: RRRRRGGGGGGBBBBB. To manipulate an individual channel you need to use a combination of bitwise operators to extract the data:

RRRRRGGGGGGBBBBB & 0000011111100000 = 00000GGGGGG00000
000000GGGGGG00000 >> 5 = 0000000000GGGGGG


Once you've moved the desired channel to the LSB's and zeroed out any other data you can directly read the value stored in that channel. You can then manipulate as you see fit and perform the reverse operation to merge it back together:

0000000000GGGGGG << 5 = 000000GGGGGG00000
RRRRRGGGGGGBBBBB & 1111100000011111 = RRRRR000000BBBBB
RRRRR000000BBBBB | 000000GGGGGG00000 = RRRRRGGGGGGBBBBB


There are many variations on this method - depending on which (if not all) channels you want to manipulate and what format the raw data is in. If you're not confident with bitwise operations then drawing out the above calculations on paper can make writing the code much easier.

Pay particular attention to the width of each channel (in the above example, either 5 or 6 bits) as you can easily overflow these quantities when performing calculations. Depending on what computations you are doing, some sort of "up scale" and "down scale" operation might be desirable - e.g. compute all values as 32bit floating point values and then scale back down to 8, 6, 5 or 4 bit integer quantities.

As a final warning - be wary of Mip-Mapping! Locking and manipulating a particular surface is not automatically reflected across other levels. This can be particularly difficult to debug - if you lock the top-level and then scale the geometry and find that the original/older texture appears you have probably got out-of-sync mip-map levels. Some hardware allows for automatic (re-)generation (see Automatic Generation of Mipmaps in the SDK documentation), in all other cases you are responsible for propagating any changes through all levels.

Useful D3DX Helper Functions

The D3DXFillTexture(), D3DXFillCubeTexture(), D3DXFillVolumeTexture() and their "Texture Shader" (the same names with a 'TX' at the end) equivalents can be very useful. They aren't always appropriate, nor are they going to be as fast as other solutions, but they hand much of the complexity over to D3DX. A simple callback function allows you to write data to a resource and have D3DX map it to the correct format and compensate for the surface's pitch.

When dealing with floating-point data and HDR Rendering you may need to decode half-precision floating point values. The D3DXFLOAT16, D3DXVECTOR2_16F, D3DXVECTOR3_16F and D3DXVECTOR4_16F types are particular useful. If a full conversion is needed then the D3DXFloat16To32Array() and D3DXFloat32To16Array() functions can be useful.
Sign in to follow this  


6 Comments


Recommended Comments

Quote:
Original post by Daerax
You wrote all that in an hour??!
Well I wasn't timing myself, but I figure it was around an hour...

Quote:
Original post by Daerax
why dont you just write a book?
Books take much more effort - I did talk to some publishers a few years back, but came to the conclusion that I definitely didn't have enough spare time.

One day though... Two things I wanna do before I retire: Write a (technical) book and give a talk/presentation at a technical conference. Not too much to ask I hope [grin]

Jack

Share this comment


Link to comment
Damn, Jack's on crack! There's no way on earth someone can produce exceptional articles at this rate.

I've not even read the whole thing (no time now), but one quick comment: VOID makes me want to kill someone -- it offers nothing at all over the standard lowercase void [smile]

Share this comment


Link to comment
That's an excellent FAQ entry. Perhaps it isn't even a frequently asked question, more of an in-depth advanced question. IAQ. =)

I can't believe I never noticed that your code is so ugly! Just kidding!! I know it's just a draft, but as a perfectionist, I would say those unnecessary spaces, caps, -comments- etc. don't look that good.
I would go for the Visual C# codingstyle.

Share this comment


Link to comment
Quote:
Original post by Muhammad Haggag
Damn, Jack's on crack! There's no way on earth someone can produce exceptional articles at this rate.
[lol] I'm sure I'll run out of energy soon enough...

Quote:
Original post by Muhammad Haggag
I've not even read the whole thing (no time now), but one quick comment: VOID makes me want to kill someone -- it offers nothing at all over the standard lowercase void [smile]
Yeah, the whole void/VOID thing annoys me as well - I tend to use lower case void in my code. I opted for the VOID notation simply because it matches the API declaration. And to annoy you of course [razz]

Quote:
Original post by Pipo DeClown
I can't believe I never noticed that your code is so ugly! Just kidding!! I know it's just a draft, but as a perfectionist, I would say those unnecessary spaces, caps, -comments- etc. don't look that good.
I would go for the Visual C# codingstyle.
I'm not familiar with the C# coding style (yet)... The 'source' tags add a bit of extra spacing at the top/bottom, but the rest is fairly compact and my standard way of writing C++ (which I corrected to match the MS/SDK style when I submitted by "HDRDemo"). How would you rewrite the code?

Cheers,
Jack

Share this comment


Link to comment
/*

Ken:
In the code below we are
generating world peace
using the PS3's Cell-CPU.
*/

if( x == 1 && y == 2 )
{
SetInt( &z, 3 );
}

vector< Wii > JustForFun;

JustForFun.push( new Wii() );

JustForFun.begin().DontSeperateLinesUnnecessarily();



versus

/*

Ken:
In the code below we are
generating world peace
using the PS3's Cell-CPU.
*/

if (x == 1 && y == 2)
{
SetInt(&z, 3);
}

vector<Wii> JustForFun;

JustForFun.push(new Wii());
JustForFun.begin().DontSeperateLinesUnnecessarily();

Share this comment


Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now