diff --git a/TheChef b/TheChef index 0273340..1981a21 160000 --- a/TheChef +++ b/TheChef @@ -1 +1 @@ -Subproject commit 0273340e3b086bf27192ae0f3019a3c2ff68b83d +Subproject commit 1981a21481b486e4995eec97cb5ec9158c62d287 diff --git a/destrum/include/destrum/Assets/TextureFile.h b/destrum/include/destrum/Assets/TextureFile.h index 1c6d372..7898f60 100644 --- a/destrum/include/destrum/Assets/TextureFile.h +++ b/destrum/include/destrum/Assets/TextureFile.h @@ -14,6 +14,7 @@ namespace TextureFile { enum class PixelFormat : std::uint32_t { RGBA16Float, + RGBA8, }; struct Texture { @@ -26,6 +27,7 @@ namespace TextureFile { [[nodiscard]] std::size_t BytesPerPixel(PixelFormat format); [[nodiscard]] Texture FromRGBA32Float(std::uint32_t width, std::uint32_t height, std::span rgba); + [[nodiscard]] Texture FromRGBA8(std::uint32_t width, std::uint32_t height, std::span rgba); [[nodiscard]] std::vector Write(const Texture& texture); [[nodiscard]] Texture Read(std::span bytes); diff --git a/destrum/src/Assets/TextureFile.cpp b/destrum/src/Assets/TextureFile.cpp index c1dc580..7a6d085 100644 --- a/destrum/src/Assets/TextureFile.cpp +++ b/destrum/src/Assets/TextureFile.cpp @@ -21,6 +21,8 @@ std::size_t TextureFile::BytesPerPixel(PixelFormat format) { switch (format) { case PixelFormat::RGBA16Float: return 4 * sizeof(std::uint16_t); + case PixelFormat::RGBA8: + return 4; } throw std::runtime_error("Unknown cooked texture pixel format"); } @@ -46,6 +48,19 @@ TextureFile::Texture TextureFile::FromRGBA32Float(std::uint32_t width, std::uint return texture; } +TextureFile::Texture TextureFile::FromRGBA8(std::uint32_t width, std::uint32_t height, std::span rgba) { + if (rgba.size() != std::size_t{width} * height * 4) { + throw std::invalid_argument("RGBA pixel data doesn't match the texture size"); + } + + return Texture{ + .width = width, + .height = height, + .format = PixelFormat::RGBA8, + .pixels = {rgba.begin(), rgba.end()}, + }; +} + std::vector TextureFile::Write(const Texture& texture) { if (texture.pixels.size() != ExpectedByteSize(texture)) { throw std::invalid_argument("Texture pixel data doesn't match its size and format"); diff --git a/destrum/src/Graphics/ImageLoader.cpp b/destrum/src/Graphics/ImageLoader.cpp index 3b03e14..3f957da 100644 --- a/destrum/src/Graphics/ImageLoader.cpp +++ b/destrum/src/Graphics/ImageLoader.cpp @@ -53,19 +53,44 @@ namespace util return ext == TextureFile::Extension; } - static VkFormat toVkFormat(TextureFile::PixelFormat format) + static VkFormat toVkFormat(TextureFile::PixelFormat format, TextureIntent intent) { switch (format) { case TextureFile::PixelFormat::RGBA16Float: return VK_FORMAT_R16G16B16A16_SFLOAT; + case TextureFile::PixelFormat::RGBA8: + return intent == TextureIntent::ColorSrgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM; } return VK_FORMAT_UNDEFINED; } + // TheChef writes .dtex next to the copied source, so every caller that has the source path gets it + static std::filesystem::path findCookedSibling(const std::filesystem::path& p) + { + std::filesystem::path cooked = p; + cooked += TextureFile::Extension; + + std::error_code error; + if (!std::filesystem::is_regular_file(cooked, error)) { + return {}; + } + const auto sourceTime = std::filesystem::last_write_time(p, error); + if (!error && sourceTime > std::filesystem::last_write_time(cooked, error)) { + return {}; + } + return cooked; + } + ImageData loadImage(const std::filesystem::path& p, TextureIntent intent) { ImageData data; - + if (!isCookedTextureExt(p)) { + if (const auto cooked = findCookedSibling(p); !cooked.empty()) { + if (ImageData cookedData = loadImage(cooked, intent); cookedData.byteSize != 0) { + return cookedData; + } + } + } if (isCookedTextureExt(p)) { try { @@ -77,7 +102,7 @@ namespace util data.height = static_cast(texture.height); data.channels = 4; - data.vkFormat = toVkFormat(texture.format); + data.vkFormat = toVkFormat(texture.format, intent); data.byteSize = data.cookedPixels.size(); } catch (const std::exception& exception) { spdlog::error("Failed to read cooked texture '{}': {}", p.string(), exception.what()); diff --git a/tests/assets_tests.cpp b/tests/assets_tests.cpp index db2accc..da48ecf 100644 --- a/tests/assets_tests.cpp +++ b/tests/assets_tests.cpp @@ -432,6 +432,32 @@ namespace { "cooked texture pixels must be uploaded as stored"); } + void testLdrTextureFile() + { + const std::vector rgba{255, 0, 0, 255, 0, 128, 255, 64}; + const auto texture = TextureFile::FromRGBA8(2, 1, rgba); + const auto loaded = TextureFile::Read(TextureFile::Write(texture)); + check(loaded.format == TextureFile::PixelFormat::RGBA8 && loaded.pixels == rgba, + "8 bit cooked textures must round-trip"); + + const auto dir = std::filesystem::temp_directory_path() / "destrum_ldr_texture_test"; + std::filesystem::create_directories(dir); + const auto source = dir / "missing_source.png"; + auto cooked = source; + cooked += TextureFile::Extension; + TextureFile::WriteToFile(texture, cooked); + + const auto color = util::loadImage(source, TextureIntent::ColorSrgb); + const auto data = util::loadImage(source, TextureIntent::DataLinear); + std::filesystem::remove_all(dir); + + check(color.vkFormat == VK_FORMAT_R8G8B8A8_SRGB && data.vkFormat == VK_FORMAT_R8G8B8A8_UNORM, + "8 bit cooked textures must pick their color space from the intent"); + check(color.width == 2 && color.height == 1 && color.pixels != nullptr && + std::memcmp(color.pixels, rgba.data(), rgba.size()) == 0, + "loading a source image must use its cooked sibling"); + } + void testAssetReferenceCacheKeys() { const AssetReference reference{ @@ -528,6 +554,7 @@ void RunAssetsTests() testAssetManagerLoadsCookedModels(); testTextureFileRoundTrip(); testImageLoaderReadsCookedTextures(); + testLdrTextureFile(); testAssetReferenceCacheKeys(); testAssetGuids(); testAssetDatabaseFollowsMovedAssets();