From a38138582ec76d1e651184d5646ef968573392d9 Mon Sep 17 00:00:00 2001 From: Alberto Torres Date: Tue, 18 Mar 2025 17:05:13 +0100 Subject: [PATCH] Add support for OpenImageIO. --- libs/oiio/oiio.cpp | 308 +++++++++++++++++++++++++++++ libs/oiio/oiio.nim | 75 +++++++ libs/packages/oiio.nim | 2 + src/gpu_formats/texture_decode.nim | 95 +++++---- src/graphics/texture.nim | 6 +- 5 files changed, 450 insertions(+), 36 deletions(-) create mode 100644 libs/oiio/oiio.cpp create mode 100644 libs/oiio/oiio.nim create mode 100644 libs/packages/oiio.nim diff --git a/libs/oiio/oiio.cpp b/libs/oiio/oiio.cpp new file mode 100644 index 0000000..33ae858 --- /dev/null +++ b/libs/oiio/oiio.cpp @@ -0,0 +1,308 @@ + +enum ImageFormat { + FORMAT_UNKNOWN = 0, + FORMAT_BYTE, // 8-bit unsigned + FORMAT_SHORT, // 16-bit unsigned + FORMAT_HALF, // 16-bit float + FORMAT_FLOAT, // 32-bit float + MAX_ENUM = 0x7FFFFFFF +}; + +// image_decoder.cpp +#include +#include +#include +#include +#include +#include +#include + +using namespace OIIO; + +static ImageFormat type_to_format(TypeDesc type) { + switch (type.basetype) { + case TypeDesc::UINT8: return FORMAT_BYTE; + case TypeDesc::UINT16: return FORMAT_SHORT; + case TypeDesc::HALF: return FORMAT_HALF; + case TypeDesc::FLOAT: return FORMAT_FLOAT; + default: return FORMAT_UNKNOWN; + } +} + +static const char* type_to_string(TypeDesc type) { + switch (type.basetype) { + case TypeDesc::UINT8: return "FORMAT_BYTE"; + case TypeDesc::UINT16: return "FORMAT_SHORT"; + case TypeDesc::HALF: return "FORMAT_HALF"; + case TypeDesc::FLOAT: return "FORMAT_FLOAT"; + default: return "FORMAT_UNKNOWN"; + } +} + +static TypeDesc format_to_type(enum ImageFormat format) { + switch (format) { + case FORMAT_BYTE: return TypeDesc::UINT8; // 8-bit unsigned + case FORMAT_SHORT: return TypeDesc::UINT16; // 16-bit unsigned + case FORMAT_HALF: return TypeDesc::HALF; // 16-bit float + case FORMAT_FLOAT: return TypeDesc::FLOAT; // 32-bit float + default: return TypeDesc::UNKNOWN; // Unknown format + } +} + +extern "C" { + +int oiio_image_decode(const uint8_t* input, size_t input_len, + uint8_t* output, size_t output_len, + int32_t desired_channels, ImageFormat format, char* is_BGR, const char* file_name) { + if (!input || !output || desired_channels < 1 || desired_channels > 4) { + fprintf(stderr, "Invalid input parameters\n"); + return 0; + } + + uint8_t* decoded = output; + bool free_decoded = false; + + try { + auto mem_reader = Filesystem::IOMemReader(input, input_len); + + auto in = ImageInput::open(file_name, nullptr, &mem_reader); + if (!in) { + fprintf(stderr, "Open failed: %s\n", geterror().c_str()); + return 0; + } + + const ImageSpec& spec = in->spec(); + *is_BGR = spec.channelnames[0] == "B"? 1:0; + const size_t required = spec.width * spec.height * desired_channels; + const TypeDesc ftype = format_to_type(format); + if (output_len != required * ftype.size()) { + fprintf(stderr, "Buffer size mismatch: Needed %zu, got %u\n", + required * ftype.size(), output_len); + in->close(); + return 0; + } + + if(spec.nchannels != desired_channels){ + // have a separate buffer for the decoded image, free later + decoded = (uint8_t*)malloc(spec.image_bytes()); + free_decoded = true; + } + + if (!in->read_image(0, 0, 0, spec.nchannels, ftype, decoded)) { + fprintf(stderr, "Read error: %s\n", in->geterror().c_str()); + in->close(); + if(free_decoded) free(decoded); + return 0; + } + + if(spec.nchannels < desired_channels){ + // expand channels + ImageSpec src_spec(spec.width, spec.height, spec.nchannels, ftype); + ImageBuf src_buf(src_spec, decoded); + ImageSpec dst_spec(spec.width, spec.height, desired_channels, ftype); + ImageBuf dst_buf(dst_spec, output); + + const float channel_values[] = {0.0f, 0.0f, 0.0f, 1.0f}; + // -1 below fills with the values above + std::array channel_order = {-1, -1, -1, -1}; + for(int i=0; i= 3){ + // single channel exception: make it greyscale RGB/RGBA + channel_order = {0,0,0,-1}; + } + + if (!ImageBufAlgo::channels(dst_buf, src_buf, + desired_channels, + channel_order, channel_values)) { + fprintf(stderr, "Channel conversion failed: %s\n", dst_buf.geterror().c_str()); + in->close(); + if(free_decoded) free(decoded); + return 0; + } + uint8_t* pixels = (uint8_t*)dst_buf.localpixels(); + if(pixels != output){ + // it turns out channels() always resets the buffer + // so it's no longer wrapping output + memcpy(output, pixels, dst_spec.image_bytes()); + } + } + + in->close(); + if(free_decoded) free(decoded); + return 1; + } + catch (const std::exception& e) { + fprintf(stderr, "Exception: %s\n", e.what()); + if(free_decoded) free(decoded); + return 0; + } + catch (...) { + fprintf(stderr, "Unknown exception occurred\n"); + if(free_decoded) free(decoded); + return 0; + } +} + +int oiio_image_get_attributes(const uint8_t* input, size_t input_len, + int32_t* width, int32_t* height, int32_t* channels, + enum ImageFormat* format, const char* file_name) { + if (!input) { + fprintf(stderr, "Null input buffer\n"); + return 0; + } + + try { + auto mem_reader = Filesystem::IOMemReader(input, input_len); + + auto in = ImageInput::open(file_name, nullptr, &mem_reader); + if (!in) { + fprintf(stderr, "Open failed: %s\n", geterror().c_str()); + return 0; + } + + const ImageSpec& spec = in->spec(); + if (width) *width = spec.width; + if (height) *height = spec.height; + if (channels) *channels = spec.nchannels; + if (format) *format = type_to_format(spec.format); + + in->close(); + return 1; + } + catch (const std::exception& e) { + fprintf(stderr, "Exception: %s\n", e.what()); + return 0; + } + catch (...) { + fprintf(stderr, "Unknown exception occurred\n"); + return 0; + } +} + +} // extern "C" + + + +// // image_to_ppm.c +// #include +// #include + +// int main(int argc, char** argv) { +// if (argc != 2) { +// fprintf(stderr, "Usage: %s \n", argv[0]); +// return 1; +// } + +// // Read input file +// FILE* file = fopen(argv[1], "rb"); +// if (!file) { +// perror("Failed to open file"); +// return 1; +// } + +// fseek(file, 0, SEEK_END); +// long file_size = ftell(file); +// fseek(file, 0, SEEK_SET); + +// uint8_t* input = (uint8_t*)malloc(file_size); +// if (!input) { +// fclose(file); +// fprintf(stderr, "Memory allocation failed\n"); +// return 1; +// } + +// if (fread(input, 1, file_size, file) != file_size) { +// fclose(file); +// free(input); +// fprintf(stderr, "File read error\n"); +// return 1; +// } +// fclose(file); + +// // Get image attributes +// int width, height, channels; +// enum ImageFormat format; +// if (!image_get_attributes(input, file_size, &width, &height, &channels, &format)) { +// free(input); +// fprintf(stderr, "Unsupported image format\n"); +// return 1; +// } + +// // Allocate output buffer for 3-channel RGB +// size_t output_size = width * height * 3; +// uint8_t* pixels = (uint8_t*)malloc(output_size); +// if (!pixels) { +// free(input); +// fprintf(stderr, "Output buffer allocation failed\n"); +// return 1; +// } + +// // Decode to 3 channels (RGB) +// if (!image_decode(input, file_size, pixels, output_size, 3, format)) { +// free(input); +// free(pixels); +// fprintf(stderr, "Image decoding failed\n"); +// return 1; +// } + +// free(input); + +// // Output PPM header +// printf("P6\n%d %d\n255\n", width, height); + +// // Output raw pixel data +// fwrite(pixels, 1, output_size, stdout); + +// free(pixels); +// return 0; +// } + +bool convertRGBToRGBA(const std::string& filename, + void* output_buffer, + size_t buffer_length, + std::string& error_msg) { + // Load source image + ImageBuf src_buf(filename); + if (!src_buf.read()) { + error_msg = "Failed to load image: " + src_buf.geterror(); + return false; + } + + const ImageSpec& src_spec = src_buf.spec(); + + // Verify source is RGB + if (src_spec.nchannels != 3) { + error_msg = "Image is not RGB (has " + std::to_string(src_spec.nchannels) + " channels)"; + return false; + } + + // Calculate required buffer size + TypeDesc data_type = src_spec.format; + const size_t required_size = src_spec.width * src_spec.height * 4 * data_type.size(); + if (buffer_length < required_size) { + error_msg = "Buffer too small. Required: " + std::to_string(required_size) + + ", provided: " + std::to_string(buffer_length); + return false; + } + + // Prepare destination buffer wrapped in ImageBuf + ImageSpec dst_spec(src_spec.width, src_spec.height, 4, data_type); + ImageBuf dst_buf(dst_spec, output_buffer); + + // Set up channel remapping with alpha=1.0 + const int channel_order[] = {0, 1, 2, -1}; // Source RGB, new alpha + const float channel_values[] = {0.0f, 0.0f, 0.0f, 1.0f}; + + // Perform channel conversion + if (!ImageBufAlgo::channels(dst_buf, src_buf, + /* channel count */ 4, + channel_order, channel_values)) { + error_msg = "Channel conversion failed: " + dst_buf.geterror(); + return false; + } + + return true; +} + diff --git a/libs/oiio/oiio.nim b/libs/oiio/oiio.nim new file mode 100644 index 0000000..251d135 --- /dev/null +++ b/libs/oiio/oiio.nim @@ -0,0 +1,75 @@ + +import std/strformat + +{.compile("oiio.cpp","-std=c++14").} +{.passL: "-lOpenImageIO -lOpenImageIO_Util".} + +type OiioImageFormat* {.size: 4.} = enum + FORMAT_UNKNOWN = 0 + FORMAT_BYTE # 8-bit unsigned + FORMAT_SHORT # 16-bit unsigned + FORMAT_HALF # 16-bit float + FORMAT_FLOAT # 32-bit float + +template size*(format: OiioImageFormat): int = + case format: + of FORMAT_BYTE: 1 + of FORMAT_SHORT: 2 + of FORMAT_HALF: 2 + of FORMAT_FLOAT: 4 + else: 0 + +proc decode(input: pointer, input_len: int, + output: pointer, output_len: int, + desired_channels: int32, format: OiioImageFormat, is_BGR: var byte, + file_name: cstring): int32 {.importc:"oiio_image_decode", cdecl.} + +proc get_attributes(input: pointer, input_len: int, + width: var int32, height: var int32, + channels: var int32, format: var OiioImageFormat, + file_name: cstring): int32 {.importc:"oiio_image_get_attributes", cdecl.} + +template has_bytes(p: pointer, bytes: string): bool = + cmpMem(p, bytes.cstring, bytes.len) == 0 + +proc file_name_from_magic(input: openArray[byte]): string = + if input.len < 10: + return "image" + var p = input[0].addr + # OpenImageIO usually has no trouble detecting the most common formats, + # however it seems we need to give it a file name for some, like EXR. + if p.has_bytes "\x89PNG": return "image.png" + if p.has_bytes "\xFF\xD8\xFF": return "image.jpg" + if p.has_bytes "v/1\x01": return "image.exr" + if p.has_bytes "#?RADIANCE": return "image.hdr" + return "image" + +proc oiioImageGetAttributes*(input: openArray[byte]): (int, int, int, OiioImageFormat) = + # returns: width, height, channels, format + doAssert input.len != 0, "input is empty" + let file_name = file_name_from_magic(input) + var w,h,c: int32 + var f: OiioImageFormat + var res = get_attributes(input[0].addr, input.len, w,h,c,f, file_name.cstring) + doAssert res != 0, "error getting image attibutes" + doAssert f != FORMAT_UNKNOWN, "unknown image format" + return (w.int, h.int, c.int, f) + +proc oiioImageDecode*[T](input: openArray[byte], min_channels = 0): seq[T] = + doAssert input.len != 0, "input is empty" + let file_name = file_name_from_magic(input) + var w,h,c: int32 + var f: OiioImageFormat + var res = get_attributes(input[0].addr, input.len, w,h,c,f, file_name.cstring) + doAssert res != 0, "error getting image attibutes" + doAssert w != 0 and h != 0 and c != 0, "invalid image" + let channels = max(min_channels.int32, c) + # let size_in = w * h * c + let size_out = w * h * channels + result.setLen size_out * f.size + var is_BGR: byte + res = decode(input[0].addr, input.len, + result[0].addr, size_out * f.size, + channels, f, is_BGR, file_name.cstring) + doAssert res != 0, "error decoding image" + diff --git a/libs/packages/oiio.nim b/libs/packages/oiio.nim new file mode 100644 index 0000000..5042b0e --- /dev/null +++ b/libs/packages/oiio.nim @@ -0,0 +1,2 @@ +import ../oiio/oiio +export oiio diff --git a/src/gpu_formats/texture_decode.nim b/src/gpu_formats/texture_decode.nim index 9c3b105..4588179 100644 --- a/src/gpu_formats/texture_decode.nim +++ b/src/gpu_formats/texture_decode.nim @@ -41,11 +41,14 @@ import vmath except Quat, quat import arr_ref import float16 -import stb_image/read as stbi -const myouConvertHdrToFloat16 {.booldefine.} = true +const myouConvertHdrToFloat16 {.booldefine.} = not defined(myouUseOpenImageIO) -when not defined(nimdoc): - import tinyexr +when defined(myouUseOpenImageIO): + import oiio +else: + import stb_image/read as stbi + when not defined(nimdoc): + import tinyexr proc stride*(format: TextureFormat): int = case format: @@ -106,7 +109,19 @@ proc f32_to_f16(source: ptr UncheckedArray[float32], dest: ptr UncheckedArray[Fl dest[i] = source[i].tofloat16(clamp=true) proc getDimensionsFormat*(p: pointer, len: int, min_channels=0): (int, int, TextureFormat) = - when not defined(nimdoc): + when defined(myouUseOpenImageIO): + var (width, height, channels, fmt) = oiioImageGetAttributes(p.toOpenArrayByte(0, len-1)) + channels = max(min_channels, channels) + when myouConvertHdrToFloat16: + if fmt == FORMAT_FLOAT: fmt = FORMAT_HALF + let format = case fmt: + of FORMAT_BYTE: (R_u8.int - 1 + channels).TextureFormat + of FORMAT_SHORT: (R_u16.int - 1 + channels).TextureFormat + of FORMAT_HALF: (R_f16.int - 1 + channels).TextureFormat + of FORMAT_FLOAT: (R_f32.int - 1 + channels).TextureFormat + else: R_u8 + return (width, height, format) + elif not defined(nimdoc): if isEXR(p, len): let dims = getEXRDimensions(p, len) return (dims[0], dims[1], RGBA_f16) @@ -144,42 +159,54 @@ proc loadFileFromSlices*(tex: Texture, slices: seq[SliceMem[byte]], var out_buffer = newSliceMem[byte](layer_stride * tex.depth) var pos = 0 for slice in slices: - var image: imagePixelData[byte] - var image_16: imagePixelData[uint16] - var image_f: imagePixelData[float32] - var buffer: ArrRef[byte] - # a reference to this pointer is kept with one of the vars above var pixels_ptr: pointer var pixels_len: int - var flip = flip - if isEXR(slice.data, slice.byte_len): - let (width, height, pixels) = decodeEXR(slice.data, slice.byte_len) - assert width == tex.width and height == tex.height, "Image size mismatch" - buffer = pixels.to byte - pixels_ptr = buffer[0].addr - pixels_len = buffer.len - else: - setFlipVerticallyOnLoad(flip) - flip = false - var w,h,c = 0 - if isHDRFromMemory(slice.toOpenArrayByte): - image_f = loadFFromMemory(slice.toOpenArrayByte, w,h,c, min_channels) - pixels_ptr = image_f.data - pixels_len = image_f.byteLen - when myouConvertHdrToFloat16: + when defined(myouUseOpenImageIO): + let image = oiioImageDecode[byte](slice.toOpenArrayByte, min_channels) + pixels_ptr = image[0].addr + pixels_len = image.len + when myouConvertHdrToFloat16: + if pixels_len == layer_stride * 2: f32_to_f16( cast[ptr UncheckedArray[float32]](pixels_ptr), cast[ptr UncheckedArray[Float16]](pixels_ptr), - image_f.len) + pixels_len div 4) pixels_len = pixels_len div 2 - elif is16BitFromMemory(slice.toOpenArrayByte): - image_16 = load16FromMemory(slice.toOpenArrayByte, w,h,c, min_channels) - pixels_ptr = image_16.data - pixels_len = image_16.byteLen + # var flip = not flip + else: + var image: imagePixelData[byte] + var image_16: imagePixelData[uint16] + var image_f: imagePixelData[float32] + var buffer: ArrRef[byte] + var flip = flip + if isEXR(slice.data, slice.byte_len): + let (width, height, pixels) = decodeEXR(slice.data, slice.byte_len) + assert width == tex.width and height == tex.height, "Image size mismatch" + buffer = pixels.to byte + pixels_ptr = buffer[0].addr + pixels_len = buffer.len else: - image = loadFromMemory(slice.toOpenArrayByte, w,h,c, min_channels) - pixels_ptr = image.data - pixels_len = image.len + setFlipVerticallyOnLoad(flip) + flip = false + var w,h,c = 0 + if isHDRFromMemory(slice.toOpenArrayByte): + image_f = loadFFromMemory(slice.toOpenArrayByte, w,h,c, min_channels) + pixels_ptr = image_f.data + pixels_len = image_f.byteLen + when myouConvertHdrToFloat16: + f32_to_f16( + cast[ptr UncheckedArray[float32]](pixels_ptr), + cast[ptr UncheckedArray[Float16]](pixels_ptr), + image_f.len) + pixels_len = pixels_len div 2 + elif is16BitFromMemory(slice.toOpenArrayByte): + image_16 = load16FromMemory(slice.toOpenArrayByte, w,h,c, min_channels) + pixels_ptr = image_16.data + pixels_len = image_16.byteLen + else: + image = loadFromMemory(slice.toOpenArrayByte, w,h,c, min_channels) + pixels_ptr = image.data + pixels_len = image.len if layer_stride != pixels_len: echo "Format: ", format raise Defect.newException &"Image '{tex.name}' has a length" & diff --git a/src/graphics/texture.nim b/src/graphics/texture.nim index 27b15ed..9970af2 100644 --- a/src/graphics/texture.nim +++ b/src/graphics/texture.nim @@ -430,8 +430,10 @@ proc newTexture*(engine: MyouEngine, name: string, self.engine.renderer.enqueue proc()= self.ensure_storage(existing_resource) if pixels != nil: + doAssert pixels.byte_len == width * height * depth * format.stride, + "Texture pixels have wrong size for the given dimensions and format" self.loadFromPixelsPointer(pixels.toPointer) - else: + elif existing_resource == nil: self.preallocate() when defined(myouUseRenderdoc): # Note: when we switch to arrays we'll have to store resolutions @@ -530,7 +532,7 @@ proc newTexture*(engine: MyouEngine, name: string, file_name: string, is_sRGB: b echo getCurrentExceptionMsg() echo "Error loading texture " & file_name else: - var (width, height, format) = getDimensionsFormat(res.orig_data.data, res.orig_data.byte_len, 3) + var (width, height, format) = getDimensionsFormat(res.orig_data.toPointer, res.orig_data.byte_len, 3) if is_sRGB: if format in [RGB_u8, RGBA_u8]: format = format.to_sRGB