Skip to content

Allow ScratchImage to be non-owning#675

Open
solbjorn wants to merge 1 commit intomicrosoft:mainfrom
solbjorn:non-owning
Open

Allow ScratchImage to be non-owning#675
solbjorn wants to merge 1 commit intomicrosoft:mainfrom
solbjorn:non-owning

Conversation

@solbjorn
Copy link

ScratchImage is very convenient as it automatically compute the number of images, pitches etc. On the other hand, when the user has an already loaded texture, it still always allocate own buffer and perform copying, which might be suboptimal, especially with large textures.

Extend ScratchImage::Initialize() with an additional argument (with the default value that behaves the same way as before) to allow it to be non-owning. This means that it still computes the number of images, pitches etc., but doesn't allocate the actual buffer. Then, the user can update individual images using ScratchImage::SetNonOwningImagePixels().
The actual ScratchImage doesn't own this memory, doesn't have own buffer and doesn't perform any deallocation on destruction. Memory management and lifetimes are fully on the user's side.

A non-owning ScratchImage differs from owning by having a non-zero m_size and m_memory == nullptr.

An example of usage:

// create ScratchImage using Initialize(meta, cp_flags, false)

// set pixels pointer for each image using
auto& image = *scratch.SetNonOwningImagePixels(mip, face, depth, ptr);

The return value is optional and is the same as ScratchImage.GetImage(mip, face, depth), only to avoid 2 external calls per subimage when needed. For example, if the user has a contiguous buffer with a mipmapped 2D image:

for (size_t i{0}, off{0}; i < scratch.GetMetadata().mipLevels; ++i)
    off += scratch.SetNonOwningImagePixels(i, 0, 0, ptr + off)->slicePitch;

A non-owning ScratchImage can currently be initialized only using Initialize() for simplicity and to indicate that the option is for advanced users only. Other Initialize*() methods can be expanded later if needed.

ScratchImage is very convenient as it automatically compute the
number of images, pitches etc. On the other hand, when the user
has an already loaded texture, it still always allocate own buffer
and perform copying, which might be suboptimal, especially with
large textures.

Extend ScratchImage::Initialize() with an additional argument (with
the default value that behaves the same way as before) to allow it
to be non-owning. This means that it still computes the number of
images, pitches etc., but doesn't allocate the actual buffer. Then,
the user can update individual images using
ScratchImage::SetNonOwningImagePixels().
The actual ScratchImage doesn't own this memory, doesn't have own
buffer and doesn't perform any deallocation on destruction. Memory
management and lifetimes are fully on the user's side.

A non-owning ScratchImage differs from owning by having a non-zero
m_size and m_memory == nullptr.

An example of usage:

// create ScratchImage using Initialize(meta, cp_flags, false)

// set pixels pointer for each image using
auto& image = *scratch.SetNonOwningImagePixels(mip, face, depth, ptr);

The return value is optional and is the same as
ScratchImage.GetImage(mip, face, depth), only to avoid 2 external
calls per subimage when needed. For example, if the user has a
contiguous buffer with a mipmapped 2D image:

for (size_t i{0}, off{0}; i < scratch.GetMetadata().mipLevels; ++i)
    off += scratch.SetNonOwningImagePixels(i, 0, 0, ptr + off)->slicePitch;

A non-owning ScratchImage can currently be initialized only using
Initialize() for simplicity and to indicate that the option is for
advanced users only. Other Initialize*() methods can be expanded
later if needed.

Signed-off-by: Alexander Lobakin <alobakin@mailbox.org>
@solbjorn
Copy link
Author

@microsoft-github-policy-service agree

@walbourn
Copy link
Member

walbourn commented Mar 2, 2026

The general model is for memory owned by the app, the Image struct is used which is why I have it as an input parameter in all the functions.

The output, however, is currently always returned as a ScratchImage which is a container for the memory which generates Image structs. IOW, the whole point of ScratchImage is to be 'owning'.

I could have function overloads that output to an Image instead of aScratchImage but this would be limited as in a number of places I rely on the fact that ScratchImage memory is laid out in a particular way.

What functions in particular are you wanting to use 'in-place' for output?

@solbjorn
Copy link
Author

solbjorn commented Mar 4, 2026

Well, I'd be glad to see some sort of function which does everything that ScratchImage::Initialize() does, but without allocating any memory. It validates the input, calculates the number of mipmaps, the total number of subimages, initializes their width/height/pitches etc, everything in the format that Dx expects. And then there's GetImage() that is very convenient to get a particular subimage.

For example, I added KTX support to my engine recently and the image layout in KTX is different there, but using a ScratchImage allocated and calculated earlier (with no allocations) allows me to assign the subimages the way that Dx needs them.
Without the change from this PR, I needed to copy each subimage/face only to call CreateTextureEx(), after which these copies are not used anymore. For 8k BC7 textures (88 Mb total) this is a big and redundant memory and CPU overhead.

https://github.com/solbjorn/reaper-engine/blob/master/ogsr_engine/Layers/xrRenderDX10/dx10Texture_ktx.cpp#L289

Dunno, maybe a version of ScratchImage::Initialize(), but as a free function and that would return an array/vector of allocated and calculated Images without allocating the memory for the textures themselves? I mean I could just open-code all those calculations, but it doesn't sound nice to me (neither having to allocate the memory twice and memcpy() 90 Mb).
And then a version of GetImage() which would take a metadata + this array of images... Sounds like a bunch of redundance, hence just non-owning ScratchImage for advanced users.

@walbourn
Copy link
Member

walbourn commented Mar 4, 2026

ScratchImage is designed to ensure the memory is laid out exactly the way a DDS file is written to disk which for the most part is how Direct3D expects 'row-linear' content to be provided as well as guarantee the memory is 16-byte aligned since DirectXTex uses SSE/SSE2 via DirectXMath.

The problem is that the metadata for the target is NOT known prior to calling the loading function or other operation. The application would have to have a way to query the metadata, then do the allocation, then call the function again to complete it.

Can you please confirm that this is limited to DDS loading, or is there some other DirectXTex functionality you are trying to use 'non-owning'?

@solbjorn
Copy link
Author

solbjorn commented Mar 4, 2026

It's limited to loading, but to loading of non-DDS texture formats using other libraries. They might have a different layout etc, but ScratchImage calculates everything in the way that DirectX needs it, and then GetImage()-like stuff allows us to map subimages properly.

I mean, I know DxTex is primarily for DDS, but OTOH it also has support for other formats and the corresponding functions also produce ScratchImage. This PR is only to make it easier to add support for more formats on the user side without having to double-allocate and copy the data. There are formats/libs like EXR which allow to perform direct reading and that's great, but some of them allocate their own internal storage / also provide only owning API and having this overhead only to create a ID3DBaseTexture and then exit is a bit frustrating.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants