-
Notifications
You must be signed in to change notification settings - Fork 649
api!: add image_span versions of ImageOutput methods #4727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
api!: add image_span versions of ImageOutput methods #4727
Conversation
* ImageOutput methods that write scanlines, tiles, and images, which are the main customization points for format output implementations, are given additional methods that take `image_view` in place of raw pointers and strides. * Generally, for each, there is a templated version that takes `image_view<T>` that communicates both the memory area and the data type conversion requested, a "base case" that takes an explicit TypeDesc for the conversion type and a `image_view<std::byte>` giving the raw memory layout, and a `cspan<T>` convenience version for when there are contiguous strides. Note that when reading mixed channel data types in "native" mode (no type conversion, just leave the data in its original types), you have to use the std::byte image_span version, since the idea is not to do any format conversion, and there may not be a single type involved. * For now, the default implementations of these new ImageOutput methods are just wrappers that call the old pointer-based ones. One by one, over time, we can swap them, changing the format implementations to have a full implementation of the new bounded versions, and make their raw pointer versions call the wrappers. The raw pointer ones will be understood to be "unsafe", still assuming that the pointers always refer to appropriately-sized memory areas. Meanwhile, the ones using spans and image_spans will, due to assertions in their implementations, make it easier to verify (at least in debug mode), that we never touch memory outside these bounds. Signed-off-by: Larry Gritz <[email protected]>
EmilDohne
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is definitely a step in the right direction, thanks a lot for writing this! I quite like the convenience methods taking only a span<T> rather than an image_span<T> in the cases were you only care about contiguous regularly strided access.
src/include/OpenImageIO/imageio.h
Outdated
| /// @param data A full description of the pixel data location, | ||
| /// dimensions, and strides. | ||
| /// @returns `true` upon success, or `false` upon failure. | ||
| template<typename T> bool write_scanline(int y, int z, image_span<T> data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[question] Is the template statement being inlined part of the clang-format doing its thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
being inlined? Yeah, I'm mostly just letting clang-format do its thing and not fighting it too hard unless it makes something confusingly unreadable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry that wasnt very clear, I meant
template <typename T> void my_functionRather than
template <typename T>
void my_functionI've just never seen that convention but if clang tidy decides its good who am I to judge :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I usually separate it to 2 lines, as you did, and so does clang-tidy usually! I don't know exactly how it decides in this case, but it's what clang-tidy did. Maybe because it's IN a class definition and is a short enough param list that it all easily fits on one line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose so yeah!
src/libOpenImageIO/imageoutput.cpp
Outdated
| std::vector<unsigned char>& scratch, unsigned int dither, | ||
| int xorigin, int yorigin, int zorigin) | ||
| { | ||
| // Eventually, we will make a fully save, span-native implementation of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] save -> safe
src/include/OpenImageIO/imageio.h
Outdated
| } | ||
|
|
||
| /// Write the full scanline that includes pixels (*,y,z), taking | ||
| /// contiguous values from a `span`. For 2D non-volume images, `z` should |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] Double space span. For
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a double space guy.
| sz, data.size_bytes()); | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[consider] I am uncertain about having a size check like this only present in debug builds (or if oiio_print_debug is defined). Is the rationale for this performance based (in which case I doubt this will add much/any overhead but would have to test)?
Either way I think this may be a bit of a slippery slope since without this check this function is free to access memory out of bounds
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm aware this is the base case and not directly intended to be used by developers but as long as its in the public API someone will use this :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're probably right here. Let me take another swing at it.
src/include/OpenImageIO/imageio.h
Outdated
| /// contiguous strides in all dimensions. This is a convenience wrapper | ||
| /// around the `write_scanlines()` that takes an `image_span<const T>`. | ||
| template<typename T> | ||
| bool write_scanlines(int ybegin, int yend, int z, TypeDesc format, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[fix] format argument is unused.
[question] Do you happen to know what flags OpenImageIO is compiled with? I'm assuming -Wall but perhaps adding -Wunused-variable (which is by default in -Wextra) would help catch such errors in the future?
If then you still want the unused variable you can use https://en.cppreference.com/w/cpp/language/attributes/maybe_unused
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, that's just an oversight, I think, good catch
src/include/OpenImageIO/imageio.h
Outdated
| auto ispan = image_span<const T>(data.data(), m_spec.nchannels, | ||
| m_spec.width, m_spec.height, | ||
| m_spec.depth); | ||
| OIIO_DASSERT(data.size_bytes() == ispan.size_bytes() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[consider] I'd maybe make this not a debug-assert but rather a contract of the function (via a check and then a return false). Since this is a fairly easy error to make as a consumer of the API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea, yeah
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, no, in this case, ispan is something we compute from the span that the user passes. It's really just a check on our own code, so OIIO_DASSERT is what I wanted here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah thats an oversight from my part, all good then!
| { | ||
| auto ispan = image_span<T>(data.data(), m_spec.nchannels, xend - xbegin, | ||
| yend - ybegin, zend - zbegin); | ||
| OIIO_DASSERT(data.size_bytes() == ispan.size_bytes() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[consider] making this a contract of the function rather than a debug assert (same as in the write_image function).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also here -- this really should be a OIIO_DASSERT, since it's just checking our own internal logic. It's not a contract because the user can't screw it up, they're passing a span and we're working with only what they told us was ok.
|
Something I was thinking about this morning... Currently, the base case with untyped memory is presented last, for each triplet of related methods. But do you think it would be more clear if I change the order to: Do you think this leads to a more clear flow of the explanation? |
Signed-off-by: Larry Gritz <[email protected]>
|
This is something im also often torn about but I'd almost veer towards the one where the base case comes first even though its the least likely you'd use but just to build it up logically. So in short, I'd go with the variant you just proposed! |
Yes, your description is exactly what I was thinking: I started by presenting the functions in the order of most likely to be used. But now I'm thinking, "how can I tell the most logical story that explains all three." Start with the fully general one with all the parameters that explains what's really going on under the hood, then show the shortcuts for common cases. |
|
I posted an update with a rearrangement of the order that the methods are presented. I start with write_image, since it's the most common case. Then within each triplet, start with a full explanation of the generic version using explicit |
For each triplet, explain in order of: * Generic: TypeDesc + image_span<byte> (full explanation) * implied type: image_span<T> (brief explanation) * implied type + contiguous: span<T> (brief explanation) Also, move write_image FIRST, followed by write_scanline(s) and write_tile(s), since the whole image will be the most common use case. Signed-off-by: Larry Gritz <[email protected]>
44fc36e to
1b94667
Compare
EmilDohne
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! The order definitely reads better like this
|
I will leave this open for any further comments or guidance until the end of the week, then (assuming no high priority change requests) I will merge and move on to ImageInput. |
|
As suspected sonar will complain about image_span for each function And none of the big 3 compilers generate particularly small code around each call site. Each generates several instructions to do the copy of the struct before making the call. I don't immediately have a good benchmark locally to see how things would play out if we just pass by reference instead. |
|
I'd be curious about benchmarks too but I very much doubt that copying around 64 bytes would make any significant difference in these functions as compression/write speeds will be the main bottlenecks. In fact, I wouldnt be surprised to see the copies be faster as the compiler may optimize other parts of the function better |
Yes, but we don't use these ONLY for functions that are doing a bunch of I/O and compression. It's used for all sorts of utility functions that just copy data around in memory, etc. It's also a pretty big PITA if the static analyzers are constantly yelling, so that's a consideration, too. I will try to put together some benchmarks of pass by value vs pass by reference. (I'm curious about what the analyzers are saying about C++23 |
|
Actually got around to doing it with a pretty bastardized version of the code: https://quick-bench.com/q/9LSLiQkta9VPl8-blkmA7sRSrDI I initially had it iterating over 3 channel and 64x64 pixels but I've reduced it to iterating over just 3 elements and it seems the difference is non-existant in both cases. |
|
I did not know about https://quick-bench.com before! (Though it doesn't seem to be able to tell us about MSVS on Windows, or in any way compare different HW architectures.) It also looks like passing by const ref has no speed penalty, so maybe that's the way to solve the problem? |
Signed-off-by: Larry Gritz <[email protected]>
|
quick-bench is really nice to quickly test out some minimum reproducible examples but as you said unfortunately it doesn't have msvc which sadly is often also the odd one out when it comes to performance. Since there is not difference between const ref or copy I don't mind either, if const ref makes sonar happy then that's a good solution :) |
I do wonder if the sonar warnings might not be outdated since I (and the compilers too apparently) would certainly not consider 64 bytes too big to copy around, unless you need nanosecond latency/performance. |
|
Thanks for that link. The blog post makes it sound like Sonar only warns on the declarations, which definitely makes it easier for us to annotate those spots with |
|
Im guessing theres no way to silence specific warnings? Im not too familiar with sonar I must admit |
|
You can silence one kind of warning everywhere, or in a list of cpp/h files. Or you can silence all warnings on one line of code with a comment formatted a specific way. There's not a way to say something like "never warn about passing this particular struct by value". And as far as I know, you can't say "ignore only this one kind of warning on this line." |
|
I made some benchmarks. The task is to sum all values in an image -- "big" is 2048x1536, so heavy on accessing data that the image span describes, and "small" is 16x16, so not much work inside the loops compared to the image_span construction and passing itself. For each, I pass the image_span by value, and pass by reference, and for each of those choices, using a single already-constructed image_span versus constructing it from the sizes and strides on each call ("imm[ediate]"). And also an old school version where there's no image_span but I pass the raw pointer, sizes, and strides as separate parameters. Caveats: VERY preliminary results, only running on one configuration (my Intel-based MacbookPro) It looks like none of these choices make a consistent big difference -- they are all clustered within the error bars of the trial-to-trial and run-to-run variation. I will try this on other platforms and alter the CI to output it for every configuration we have, maybe it'll turn something interesting up. |
Signed-off-by: Larry Gritz <[email protected]>
Signed-off-by: Larry Gritz <[email protected]>
|
I pushed an update that adds benchmarking of the pass by value vs reference issue. I had to do some experimentation to get it able to run on the CI machines, I will put that in a different PR to avoid the clutter here (but I will report the results here). Shockingly, running benchmarks on the CI runners is much better and more consistent than I would have guessed. I'm basically testing several things here at once:
Linux, Intel CPU, gcc11The above is from the GitHub runners, and for comparison, here is on my workstation at work, definitely not sharing with any other important process: the machine is obviously faster, but the results are proportional and we can see that even on the GHA runner, the trial-to-trial timing variation is really not bad at all. I think we can assume that benchmark results of this nature on GHA are reasonably valid. Mac Intel, clang (GHA)Mac ARM, clang (GHA)Windows Intel, MSVS (GHA)So as far as I can discern, the answers are:
In conclusion I'm confident that switching from pointers to image_span isn't going to hurt performance, and I don't think it matters at all (for performance) whether we pass image_span by value or by const ref, so if the latter silences the static analysis, I don't have any objections on performance grounds. What do you all think? |
|
@EmilDohne @jessey-git What's your call on what I should do here? My current thinking is that maybe it's (slightly) a better choice to change to const ref, for the following reasons:
Let me know if you think there's a compelling reason to stick to pass-by-value here. Otherwise, I'll soon change the declarations to pass by const ref and put this PR to bed so we can move to the next thing. (And remember, this is only in main, won't be truly locked down until a fall release, so the penalty for changing our mind in the next few months is very low.) |
|
That seems reasonable and yeah git isn't readonly so we can change again if needed. At some tedious busy-work expense that is. On the monday call I was mainly torn between "'spans' are typically passed by value" and "but it's really kind of chonky". So for now I'd opt for pass-by-ref and prevent the clutter from the //NOSONAR. |
|
I agree, I think we were confused by the fact that a regular |
Signed-off-by: Larry Gritz <[email protected]>
|
Updated with a change of convention to passing image_span parameters to functions as const reference. |
|
I'm also perfectly happy with pass by const ref, fine to merge by me! |
Signed-off-by: Larry Gritz <[email protected]>
eadfc2e to
6dfbf5b
Compare
|
Sorry, one more amendment: A while back, we discussed this and decided to drop the "z" parameter from the new scanline-based read/write methods, since there were no known formats that were both volumetric and scanline oriented. (The few volume formats we support only have tiles.) My old brain had forgotten that and left the z's in while working on this PR. So I'm dropping those parameters now, before they take root and I forget about them. |
Signed-off-by: Larry Gritz <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds new image_span‑based overloads for the main ImageOutput methods and updates related tests and utility functions. Key changes include:
- Adding templated image_span versions of write_image, write_scanline(s), write_tile(s), and write_rectangle.
- Updating typedesc and span APIs for improved const‐correctness and modern C++ style.
- Adjusting tests and benchmarks to work with the new span-based interfaces.
Reviewed Changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| testsuite/docs-examples-cpp/src/docs-examples-imageoutput.cpp | Updated examples to use std::vector and image_span based API calls |
| src/libutil/typedesc_test.cpp | Extended tests to cover const qualified types |
| src/libOpenImageIO/imageoutput.cpp | Introduced new image_span overloads for image output methods |
| src/libOpenImageIO/imageio.cpp | Updated contiguize and copy_image functions to accept const references |
| src/include/OpenImageIO/typedesc.h | Added const-specializations for BaseTypeFromC and updated BaseTypeFromC_v |
| src/include/OpenImageIO/span.h and image_span.h | Adjusted API signatures and dynamic_extent constant |
| CMakeLists.txt | Bumped version to 3.1.2.0 |
Comments suppressed due to low confidence (2)
src/include/OpenImageIO/span.h:43
- Changing dynamic_extent from -1 to the maximum representable value is a significant API change; please confirm that all consumers of this constant have been adjusted accordingly.
inline constexpr span_size_t dynamic_extent = std::numeric_limits<span_size_t>::max();
src/libOpenImageIO/imageoutput.cpp:292
- The write_rectangle overload currently always returns false; please add a comment or TODO to clarify that this stub is intentional until a full implementation is provided.
bool ImageOutput::write_rectangle(int /*xbegin*/, int /*xend*/, int /*ybegin*/, int /*yend*/, int /*zbegin*/, int /*zend*/, TypeDesc /*format*/, const image_span<const std::byte>& /*data*/)
|
OK, merging and moving on to the next thing! |
b356152
into
AcademySoftwareFoundation:main
…Foundation#4727) * ImageOutput methods that write scanlines, tiles, and images, which are the main customization points for format output implementations, are given additional methods that take `image_span` in place of raw pointers and strides. * Generally, for each, there is a templated version that takes `image_span<T>` that communicates both the memory area and the data type conversion requested, a "base case" that takes an explicit TypeDesc for the conversion type and a `image_view<std::byte>` giving the raw memory layout, and a `span<T>` convenience version for when there are contiguous strides. Note that when reading mixed channel data types in "native" mode (no type conversion, just leave the data in its original types), you have to use the std::byte image_span version, since the idea is not to do any format conversion, and there may not be a single type involved. * For now, the default implementations of these new ImageOutput methods are just wrappers that call the old pointer-based ones. One by one, over time, we can swap them, changing the format implementations to have a full implementation of the new bounded versions, and make their raw pointer versions call the wrappers. The raw pointer ones will be understood to be "unsafe", merely assuming that the pointers always refer to appropriately-sized memory areas. Meanwhile, the ones using spans and image_spans will, due to assertions in their implementations, make it easier to verify (at least in debug mode), that we never touch memory outside these bounds. --------- Signed-off-by: Larry Gritz <[email protected]> Signed-off-by: Scott Wilson <[email protected]>
…Foundation#4727) * ImageOutput methods that write scanlines, tiles, and images, which are the main customization points for format output implementations, are given additional methods that take `image_span` in place of raw pointers and strides. * Generally, for each, there is a templated version that takes `image_span<T>` that communicates both the memory area and the data type conversion requested, a "base case" that takes an explicit TypeDesc for the conversion type and a `image_view<std::byte>` giving the raw memory layout, and a `span<T>` convenience version for when there are contiguous strides. Note that when reading mixed channel data types in "native" mode (no type conversion, just leave the data in its original types), you have to use the std::byte image_span version, since the idea is not to do any format conversion, and there may not be a single type involved. * For now, the default implementations of these new ImageOutput methods are just wrappers that call the old pointer-based ones. One by one, over time, we can swap them, changing the format implementations to have a full implementation of the new bounded versions, and make their raw pointer versions call the wrappers. The raw pointer ones will be understood to be "unsafe", merely assuming that the pointers always refer to appropriately-sized memory areas. Meanwhile, the ones using spans and image_spans will, due to assertions in their implementations, make it easier to verify (at least in debug mode), that we never touch memory outside these bounds. --------- Signed-off-by: Larry Gritz <[email protected]> Signed-off-by: Scott Wilson <[email protected]>
ImageInput methods that read scanlines, tiles, and image are given new flavors that take image_span, much like the changes of PR #4727 did for ImageOutput. Generally, for each, there are three versions: (1) a low-level virtual method base case that takes an `image_span<byte>` that describes the underlying memory, and an explicit TypeDesc that specifies the data type conversion (including allowing TypeUnknown to indicate keeping channels in their native per-channel format from the file); (2) a templated version taking an `image_span<T>` that infers the data type conversion from `T` (must be a single type for all channels); (3) a templated version taking a `span<T>` that further implies a contiguous buffer in all dimensions. For now, the default implementations of these new ImageInput methods are just wrappers that call the old pointer-based ones. One by one, over time, we can swap them, changing the format implementations to have a full implementation of the new bounded versions, and make their raw pointer versions call the wrappers. The raw pointer ones will be understood to be "unsafe", merely assuming that the pointers always refer to appropriately-sized memory areas. Meanwhile, the ones using spans and image_spans will, due to assertions in their implementations, make it easier to verify (at least in debug mode), that we never touch memory outside these bounds. --------- Signed-off-by: Larry Gritz <[email protected]>
…Foundation#4727) * ImageOutput methods that write scanlines, tiles, and images, which are the main customization points for format output implementations, are given additional methods that take `image_span` in place of raw pointers and strides. * Generally, for each, there is a templated version that takes `image_span<T>` that communicates both the memory area and the data type conversion requested, a "base case" that takes an explicit TypeDesc for the conversion type and a `image_view<std::byte>` giving the raw memory layout, and a `span<T>` convenience version for when there are contiguous strides. Note that when reading mixed channel data types in "native" mode (no type conversion, just leave the data in its original types), you have to use the std::byte image_span version, since the idea is not to do any format conversion, and there may not be a single type involved. * For now, the default implementations of these new ImageOutput methods are just wrappers that call the old pointer-based ones. One by one, over time, we can swap them, changing the format implementations to have a full implementation of the new bounded versions, and make their raw pointer versions call the wrappers. The raw pointer ones will be understood to be "unsafe", merely assuming that the pointers always refer to appropriately-sized memory areas. Meanwhile, the ones using spans and image_spans will, due to assertions in their implementations, make it easier to verify (at least in debug mode), that we never touch memory outside these bounds. --------- Signed-off-by: Larry Gritz <[email protected]>
…oundation#4748) ImageInput methods that read scanlines, tiles, and image are given new flavors that take image_span, much like the changes of PR AcademySoftwareFoundation#4727 did for ImageOutput. Generally, for each, there are three versions: (1) a low-level virtual method base case that takes an `image_span<byte>` that describes the underlying memory, and an explicit TypeDesc that specifies the data type conversion (including allowing TypeUnknown to indicate keeping channels in their native per-channel format from the file); (2) a templated version taking an `image_span<T>` that infers the data type conversion from `T` (must be a single type for all channels); (3) a templated version taking a `span<T>` that further implies a contiguous buffer in all dimensions. For now, the default implementations of these new ImageInput methods are just wrappers that call the old pointer-based ones. One by one, over time, we can swap them, changing the format implementations to have a full implementation of the new bounded versions, and make their raw pointer versions call the wrappers. The raw pointer ones will be understood to be "unsafe", merely assuming that the pointers always refer to appropriately-sized memory areas. Meanwhile, the ones using spans and image_spans will, due to assertions in their implementations, make it easier to verify (at least in debug mode), that we never touch memory outside these bounds. --------- Signed-off-by: Larry Gritz <[email protected]>
…Foundation#4727) * ImageOutput methods that write scanlines, tiles, and images, which are the main customization points for format output implementations, are given additional methods that take `image_span` in place of raw pointers and strides. * Generally, for each, there is a templated version that takes `image_span<T>` that communicates both the memory area and the data type conversion requested, a "base case" that takes an explicit TypeDesc for the conversion type and a `image_view<std::byte>` giving the raw memory layout, and a `span<T>` convenience version for when there are contiguous strides. Note that when reading mixed channel data types in "native" mode (no type conversion, just leave the data in its original types), you have to use the std::byte image_span version, since the idea is not to do any format conversion, and there may not be a single type involved. * For now, the default implementations of these new ImageOutput methods are just wrappers that call the old pointer-based ones. One by one, over time, we can swap them, changing the format implementations to have a full implementation of the new bounded versions, and make their raw pointer versions call the wrappers. The raw pointer ones will be understood to be "unsafe", merely assuming that the pointers always refer to appropriately-sized memory areas. Meanwhile, the ones using spans and image_spans will, due to assertions in their implementations, make it easier to verify (at least in debug mode), that we never touch memory outside these bounds. --------- Signed-off-by: Larry Gritz <[email protected]>
…oundation#4748) ImageInput methods that read scanlines, tiles, and image are given new flavors that take image_span, much like the changes of PR AcademySoftwareFoundation#4727 did for ImageOutput. Generally, for each, there are three versions: (1) a low-level virtual method base case that takes an `image_span<byte>` that describes the underlying memory, and an explicit TypeDesc that specifies the data type conversion (including allowing TypeUnknown to indicate keeping channels in their native per-channel format from the file); (2) a templated version taking an `image_span<T>` that infers the data type conversion from `T` (must be a single type for all channels); (3) a templated version taking a `span<T>` that further implies a contiguous buffer in all dimensions. For now, the default implementations of these new ImageInput methods are just wrappers that call the old pointer-based ones. One by one, over time, we can swap them, changing the format implementations to have a full implementation of the new bounded versions, and make their raw pointer versions call the wrappers. The raw pointer ones will be understood to be "unsafe", merely assuming that the pointers always refer to appropriately-sized memory areas. Meanwhile, the ones using spans and image_spans will, due to assertions in their implementations, make it easier to verify (at least in debug mode), that we never touch memory outside these bounds. --------- Signed-off-by: Larry Gritz <[email protected]>

ImageOutput methods that write scanlines, tiles, and images, which are the main customization points for format output implementations, are given additional methods that take
image_spanin place of raw pointers and strides.Generally, for each, there is a templated version that takes
image_span<T>that communicates both the memory area and the data type conversion requested, a "base case" that takes an explicit TypeDesc for the conversion type and aimage_view<std::byte>giving the raw memory layout, and aspan<T>convenience version for when there are contiguous strides. Note that when reading mixed channel data types in "native" mode (no type conversion, just leave the data in its original types), you have to use the std::byte image_span version, since the idea is not to do any format conversion, and there may not be a single type involved.For now, the default implementations of these new ImageOutput methods are just wrappers that call the old pointer-based ones. One by one, over time, we can swap them, changing the format implementations to have a full implementation of the new bounded versions, and make their raw pointer versions call the wrappers. The raw pointer ones will be understood to be "unsafe", merely assuming that the pointers always refer to appropriately-sized memory areas. Meanwhile, the ones using spans and image_spans will, due to assertions in their implementations, make it easier to verify (at least in debug mode), that we never touch memory outside these bounds.