on-demand random access #226
-
Having in mind a texture engine such as I Looked in the Would there be a way to achieve on-demand loading of part of the an exr while avoiding the overhead of reopening the file and recreating a Regards. |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 15 replies
-
Hi! Your observations seem to be correct. It was planned to support this exactly as you described it, so I'm confident it's possible to to so. Unfortunately, the library does not yet offer a nice abstraction for your use case. But the basic building blocks are there, so you should be able to do it yourself. About the Unfortunately, that might not be enough though. Because with this approach, it will still construct a brand new image each time. You can try to find a way around that, but I'm not sure. I'll describe the "correct" way to do it, it will be a lot of work though. You might be able to find a simpler way. To understand how loading arbitrary pixel tiles on demand could be achieved, let's look at the structure of an exr file. An exr file consists of these three components, stored sequentially, right after another, in the file:
You need to prepare the following state (see
Keep those. Keep the byte stream open. Then, you are ready for loading any chunk you desire:
Calculating the correct indices get complicated if the file contains mip maps, or subsampling, or data windows and layer positions. That would be the main benefit of having an abstraction. I'm interested to add a nice abstraction for this to the library, but it will require some non trivial architectural work... so it's unclear whether I can do it in the near future. Do you want to attempt it yourself? |
Beta Was this translation helpful? Give feedback.
-
Playing around with the idea of having an abstraction for this. Maybe like this? Opening a file and loading pixel subrects on demand. This assumes the full texture is allocated in memory in full resolution. Is this useful? // this opens the file. it does not reading any pixels yet. the api is the same as the other api. it contains the full resolution image, but it will be empty at first.
let mut lazy_loader = exrs::image::lazy_open_rgba(
BufRead::new(File::open("large-file.exr")),
// how to construct your image.
|size, _channels| MyGpuImage::empty(size), // question: can you always immediately create a buffer? maybe gpu needs async support?
// how to insert a pixel into your image
|my_gpu_image, xy, (r,g,b,a)| my_gpu_image.set_pixel(xy, r,g,b,a),
);
// later ...
my_app.on_view_changed(|view_rectangle_bounds|{
lazy_loader.load_section(Bounds::DisplayWindow(view_rectangle_bounds)); // todo: this should be async, as exrs needs to decompress the block, which can take some time.
let indermediate_image: &MyGpuImage = &lazy_loader.intermediate_image().layer.data;
gpu_update_texture_buffer(main_texture, indermediate_image);
})
// dropping the lazy loader closes the file. Maybe a low-level abstraction would be better (is more flexible and requires not too much code): let mut seekable_bytes = File::open("large-file.exr");
let lazy_reader = exrs::image::lazy_read(&mut BufRead::new(seekable_bytes));
let mut my_gpu_image = MyGpuImage::empty(0, 0); // could also pass block_info.size already, but it would be too large
// later ...
my_app.on_view_changed(move |view_rectangle_bounds, mip_level|{
my_gpu_image.allocate_section(view_rectangle_bounds, mip_level);
lazy_reader
.read_pixel_section_rgba(
&mut seekable_bytes,
LazyLoad::OnlyNewBlocks, // or LazyLoad::NewAndExistingBlocks
// todo: can the following two simply be specified by passing a block index?
Bounds::DisplayWindow(view_rectangle_bounds),
Level::MipMap(mip_level),
// pixel closure is called for each pixel in the section (might come from multiple exr blocks)
move |x,y, (r,g,b,a)| my_gpu_image.set_pixel(x,y, r,g,b,a) // is capturing this mut reference possible with async?
)
.await
.unwrap(); // returns Err when pixel section is not inside the image
my_gpu_image.send_to_gpu();
}); Or even lower level: let mut seekable_bytes = File::open("large-file.exr");
// this reader remembers meta data and offset tables. so it knows which blocks to load for any given pixel section.
// it can remember which blocks are already loaded. it does not decompress the blocks itself.
let lazy_reader = exrs::image::lazy_read(&mut BufRead::new(seekable_bytes));
let mut my_sparse_texture = SparseTexture::empty(); // could also pass block_info.size already, but it would be too large
// later ...
my_app.on_view_changed(move |view_rectangle_bounds, mip_level|{
let decompressed_blocks: AsyncStream<UncompressedBlock> = lazy_reader
.async_pixel_section_blocks(
&mut seekable_bytes,
LazyLoad::OnlyNewBlocks, // or LazyLoad::NewAndExistingBlocks
// todo: can the following two simply be specified by passing a block index?
Bounds::DisplayWindow(view_rectangle_bounds),
Level::MipMap(mip_level),
);
for block in decompressed_blocks {
let unpacked_rgba_buffer = block.read_all_pixels_rgba(PixelVec::<[f32,f32,f32]>::new(), PixelVec::set_pixel).unwrap(); // todo: reuse pixelvec allocation
my_sparse_texture.insert_rgba_buffer_section(view_rectangle_bounds, mip_level, unpacked_rgba_buffer);
}
}); Most certainly, those two abstractions can coexist. The first on builds on the more modular second one. Would any of this work for your use case? |
Beta Was this translation helpful? Give feedback.
-
It's been a few days, I thought I would post some feedbacks. I've started to use your branch on my personal project, I can load pixels from exr on demand, it seems to work as expected, nice work! ✨ I realised, the API to select which channel to load was not as useful as I hoped. While I can understand it is very handy when we know which channel to load up front, and by name, but in some cases, it feels there's a need for an additional way to get to the channels. What about following use cases.
At the moment I am loading all channels 4 by 4, and using Note, I am making an assumption here. I am assuming that when a block is uncompressed, all channels in that block are uncompressed. Even if the channel extractor is only selecting a subset of the channels. For example, I might choose to extract channel "R", I am working on an ImageViewer and the user wants to see channel "R". I hope that made sense. :-) |
Beta Was this translation helpful? Give feedback.
-
Hi, It's been a while since I checked your branch. I resumed my experiment and noticed something about function This is in the context of accessing a mip level which is not level 0. The current block filtering logic is comparing the requested pixel_section - with the result of Checking what This does not seem to take into consideration the mip level.
This makes me realised that I"m not sure how to use function example:
I believe the user would expect to receive the single block stored at level 1. Conceptually, that block covers the whole image in this example. But in current implementation,
What would be the pixel_section that we need to pass to Or should pixel_section be defined in the level 0 space? Then, isn't cheers. |
Beta Was this translation helpful? Give feedback.
Hi! Your observations seem to be correct.
It was planned to support this exactly as you described it, so I'm confident it's possible to to so. Unfortunately, the library does not yet offer a nice abstraction for your use case. But the basic building blocks are there, so you should be able to do it yourself.
About the
block::Reader
:We can make the reader fields public. In that case, you can construct a reader from an existing meta data struct. That way you can construct a new block reader without reading meta data from the file again.
Unfortunately, that might not be enough though. Because with this approach, it will still construct a brand new image each time. You can try to find a way a…