Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions Axiom/Assets/AssetCooker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,79 @@ CookTextureAsset(const std::filesystem::path &ContentRoot,
return Entry;
}

std::optional<AssetCookManifestEntry>
CookHDRTextureAsset(const std::filesystem::path &ContentRoot,
const std::filesystem::path &RelativeAssetPath) {
const std::filesystem::path SourcePath = ContentRoot / RelativeAssetPath;
const auto SourceHash = HashFileContents(SourcePath);
if (!SourceHash.has_value()) {
A_CORE_WARN("AssetCooker: failed to hash source HDR texture '{}'",
SourcePath.string());
return std::nullopt;
}

const auto Texture = LoadHDRTextureFromSourceFile(SourcePath);
if (!Texture || !Texture->IsValid()) {
A_CORE_WARN("AssetCooker: failed to decode source HDR texture '{}'",
SourcePath.string());
return std::nullopt;
}

return CookHDRTextureAsset(ContentRoot, RelativeAssetPath, *Texture);
}

std::optional<AssetCookManifestEntry>
CookHDRTextureAsset(const std::filesystem::path &ContentRoot,
const std::filesystem::path &RelativeTexturePath,
const HDRTextureSourceData &Texture) {
const AssetId Asset = AssetIdFromRelativePath(RelativeTexturePath);
if (!Texture.IsValid()) {
A_CORE_WARN("AssetCooker: invalid generated HDR texture '{}'",
RelativeTexturePath.string());
return std::nullopt;
}

const std::filesystem::path CookedRelativePath =
BuildCookedTextureRelativePath(RelativeTexturePath);
const std::filesystem::path CookedAbsolutePath = ContentRoot / CookedRelativePath;
std::error_code Ec;
std::filesystem::create_directories(CookedAbsolutePath.parent_path(), Ec);
if (Ec) {
A_CORE_WARN("AssetCooker: failed to create cooked HDR texture directory '{}': {}",
CookedAbsolutePath.parent_path().string(), Ec.message());
return std::nullopt;
}

if (!SaveCookedHDRTextureAsset(CookedAbsolutePath, Texture, Asset)) {
return std::nullopt;
}

const std::filesystem::path ManifestPath =
ContentRoot / "Cooked" / "AssetCookManifest.json";
AssetCookManifest Manifest =
LoadAssetCookManifest(ManifestPath).value_or(AssetCookManifest{});

const uint64_t SourceHash = HashString(
std::to_string(Texture.Width) + "|" + std::to_string(Texture.Height) +
"|hdr|" + std::to_string(Texture.Pixels.size()));

AssetCookManifestEntry Entry{
.Id = Asset,
.Kind = AssetKind::Texture,
.RelativePath = RelativeTexturePath.generic_string(),
.CookedPath = CookedRelativePath.generic_string(),
.FormatVersion = kCookedTextureFormatVersion,
.SourceHash = SourceHash,
};
UpsertManifestEntry(Manifest, Entry);

if (!SaveAssetCookManifest(ManifestPath, Manifest)) {
return std::nullopt;
}

return Entry;
}

std::optional<AssetCookManifestEntry>
CookMaterialAsset(const std::filesystem::path &ContentRoot,
const std::filesystem::path &RelativeMaterialPath,
Expand Down
13 changes: 13 additions & 0 deletions Axiom/Assets/AssetCooker.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ CookTextureAsset(const std::filesystem::path &ContentRoot,
const std::filesystem::path &RelativeTexturePath,
const TextureSourceData &Texture);

// Decodes a source HDR texture (e.g. .hdr) from the content directory, writes
// a cooked v2 `.wtex` with float pixels, and updates the cook manifest.
std::optional<AssetCookManifestEntry>
CookHDRTextureAsset(const std::filesystem::path &ContentRoot,
const std::filesystem::path &RelativeAssetPath);

// Writes generated cooked HDR texture state under Content/Cooked and updates the
// cook manifest for the provided logical texture path.
std::optional<AssetCookManifestEntry>
CookHDRTextureAsset(const std::filesystem::path &ContentRoot,
const std::filesystem::path &RelativeTexturePath,
const HDRTextureSourceData &Texture);

// Writes generated cooked material state under Content/Cooked and updates the
// cook manifest for the provided logical material path.
std::optional<AssetCookManifestEntry>
Expand Down
32 changes: 32 additions & 0 deletions Axiom/Assets/CookedAssetRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,38 @@ LoadCookedTextureAssetIfAvailable(const std::filesystem::path &Path) {
return std::make_shared<TextureSourceData>(*CookedTexture);
}

HDRTextureSourceDataRef
LoadCookedHDRTextureAssetIfAvailable(const std::filesystem::path &Path) {
const auto ContentRoot = FindContentRootForPath(Path);
if (!ContentRoot.has_value()) {
return nullptr;
}

std::error_code Ec;
const auto RelativePath = std::filesystem::relative(Path, *ContentRoot, Ec);
if (Ec) {
return nullptr;
}

const CookedAssetSource CookedSource(*ContentRoot);
if (!CookedSource.HasManifest()) {
return nullptr;
}

const auto CookedPath =
CookedSource.Resolve(AssetIdFromRelativePath(RelativePath));
if (!CookedPath.has_value()) {
return nullptr;
}

const auto CookedTexture = LoadCookedHDRTextureAsset(*CookedPath);
if (!CookedTexture.has_value()) {
return nullptr;
}

return std::make_shared<HDRTextureSourceData>(*CookedTexture);
}

std::optional<CookedMaterialData>
LoadCookedMaterialAssetIfAvailable(const std::filesystem::path &Path) {
const auto ContentRoot = FindContentRootForPath(Path);
Expand Down
3 changes: 3 additions & 0 deletions Axiom/Assets/CookedAssetRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ LoadCookedMeshAssetIfAvailable(const std::filesystem::path &Path);
TextureSourceDataRef
LoadCookedTextureAssetIfAvailable(const std::filesystem::path &Path);

HDRTextureSourceDataRef
LoadCookedHDRTextureAssetIfAvailable(const std::filesystem::path &Path);

std::optional<CookedMaterialData>
LoadCookedMaterialAssetIfAvailable(const std::filesystem::path &Path);

Expand Down
171 changes: 146 additions & 25 deletions Axiom/Assets/CookedTextureAsset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,49 @@ template <typename T> bool ReadValue(std::ifstream &Stream, T &Value) {
return Stream.good();
}

bool WriteCommonHeader(std::ofstream &Stream, uint32_t Width, uint32_t Height,
uint32_t PixelByteCount, AssetId Asset) {
const FileHeader Header{
.Magic = {kMagic[0], kMagic[1], kMagic[2], kMagic[3]},
.Version = kCookedTextureFormatVersion,
.AssetIdValue = Asset.Value,
.Width = Width,
.Height = Height,
.ChannelCount = 4,
.PixelByteCount = PixelByteCount,
};
return WriteValue(Stream, Header);
}

bool ReadAndValidateHeader(std::ifstream &Stream, FileHeader &Header,
const std::filesystem::path &Path) {
if (!ReadValue(Stream, Header)) {
A_CORE_WARN("CookedTextureAsset: failed to read header from '{}'",
Path.string());
return false;
}

if (Header.Magic[0] != kMagic[0] || Header.Magic[1] != kMagic[1] ||
Header.Magic[2] != kMagic[2] || Header.Magic[3] != kMagic[3]) {
A_CORE_WARN("CookedTextureAsset: invalid magic in '{}'", Path.string());
return false;
}

if (Header.Version != 1 && Header.Version != kCookedTextureFormatVersion) {
A_CORE_WARN("CookedTextureAsset: unsupported version {} in '{}'",
Header.Version, Path.string());
return false;
}

if (Header.ChannelCount != 4) {
A_CORE_WARN("CookedTextureAsset: unsupported channel count {} in '{}'",
Header.ChannelCount, Path.string());
return false;
}

return true;
}

} // namespace

bool SaveCookedTextureAsset(const std::filesystem::path &Path,
Expand All @@ -52,16 +95,13 @@ bool SaveCookedTextureAsset(const std::filesystem::path &Path,
return false;
}

const FileHeader Header{
.Magic = {kMagic[0], kMagic[1], kMagic[2], kMagic[3]},
.Version = kCookedTextureFormatVersion,
.AssetIdValue = Asset.Value,
.Width = Texture.Width,
.Height = Texture.Height,
.ChannelCount = 4,
.PixelByteCount = static_cast<uint32_t>(Texture.Pixels.size()),
};
if (!WriteValue(Stream, Header))
if (!WriteCommonHeader(Stream, Texture.Width, Texture.Height,
static_cast<uint32_t>(Texture.Pixels.size()), Asset))
return false;

const auto Format =
static_cast<uint32_t>(CookedTexturePixelFormat::RGBA8);
if (!WriteValue(Stream, Format))
return false;

Stream.write(reinterpret_cast<const char *>(Texture.Pixels.data()),
Expand All @@ -77,42 +117,123 @@ LoadCookedTextureAsset(const std::filesystem::path &Path) {
}

FileHeader Header{};
if (!ReadValue(Stream, Header)) {
A_CORE_WARN("CookedTextureAsset: failed to read header from '{}'",
if (!ReadAndValidateHeader(Stream, Header, Path)) {
return std::nullopt;
}

CookedTexturePixelFormat PixelFormat = CookedTexturePixelFormat::RGBA8;
if (Header.Version >= 2) {
uint32_t FormatRaw = 0;
if (!ReadValue(Stream, FormatRaw)) {
A_CORE_WARN("CookedTextureAsset: failed to read pixel format from '{}'",
Path.string());
return std::nullopt;
}
PixelFormat = static_cast<CookedTexturePixelFormat>(FormatRaw);
}

if (PixelFormat != CookedTexturePixelFormat::RGBA8) {
A_CORE_WARN("CookedTextureAsset: '{}' is an HDR texture; use "
"LoadCookedHDRTextureAsset instead",
Path.string());
return std::nullopt;
}

if (Header.Magic[0] != kMagic[0] || Header.Magic[1] != kMagic[1] ||
Header.Magic[2] != kMagic[2] || Header.Magic[3] != kMagic[3]) {
A_CORE_WARN("CookedTextureAsset: invalid magic in '{}'", Path.string());
TextureSourceData Texture;
Texture.Width = Header.Width;
Texture.Height = Header.Height;
Texture.Pixels.resize(Header.PixelByteCount);
Stream.read(reinterpret_cast<char *>(Texture.Pixels.data()),
static_cast<std::streamsize>(Texture.Pixels.size()));
if (!Stream.good()) {
return std::nullopt;
}

if (Header.Version != kCookedTextureFormatVersion) {
A_CORE_WARN("CookedTextureAsset: unsupported version {} in '{}'",
Header.Version, Path.string());
if (!Texture.IsValid()) {
A_CORE_WARN("CookedTextureAsset: decoded invalid texture payload from '{}'",
Path.string());
return std::nullopt;
}

if (Header.ChannelCount != 4) {
A_CORE_WARN("CookedTextureAsset: unsupported channel count {} in '{}'",
Header.ChannelCount, Path.string());
return Texture;
}

bool SaveCookedHDRTextureAsset(const std::filesystem::path &Path,
const HDRTextureSourceData &Texture,
AssetId Asset) {
if (!Texture.IsValid()) {
A_CORE_WARN("CookedTextureAsset: refusing to save invalid HDR texture '{}'",
Path.string());
return false;
}

std::ofstream Stream(Path, std::ios::binary);
if (!Stream.is_open()) {
A_CORE_ERROR("CookedTextureAsset: could not open '{}' for writing",
Path.string());
return false;
}

const uint32_t PixelByteCount =
static_cast<uint32_t>(Texture.Pixels.size() * sizeof(float));
if (!WriteCommonHeader(Stream, Texture.Width, Texture.Height, PixelByteCount,
Asset))
return false;

const auto Format =
static_cast<uint32_t>(CookedTexturePixelFormat::RGBA32F);
if (!WriteValue(Stream, Format))
return false;

Stream.write(reinterpret_cast<const char *>(Texture.Pixels.data()),
static_cast<std::streamsize>(PixelByteCount));
return Stream.good();
}

std::optional<HDRTextureSourceData>
LoadCookedHDRTextureAsset(const std::filesystem::path &Path) {
std::ifstream Stream(Path, std::ios::binary);
if (!Stream.is_open()) {
return std::nullopt;
}

TextureSourceData Texture;
FileHeader Header{};
if (!ReadAndValidateHeader(Stream, Header, Path)) {
return std::nullopt;
}

if (Header.Version < 2) {
A_CORE_WARN("CookedTextureAsset: '{}' is a v1 LDR texture, not HDR",
Path.string());
return std::nullopt;
}

uint32_t FormatRaw = 0;
if (!ReadValue(Stream, FormatRaw)) {
A_CORE_WARN("CookedTextureAsset: failed to read pixel format from '{}'",
Path.string());
return std::nullopt;
}

if (static_cast<CookedTexturePixelFormat>(FormatRaw) !=
CookedTexturePixelFormat::RGBA32F) {
A_CORE_WARN("CookedTextureAsset: '{}' is not an HDR texture", Path.string());
return std::nullopt;
}

HDRTextureSourceData Texture;
Texture.Width = Header.Width;
Texture.Height = Header.Height;
Texture.Pixels.resize(Header.PixelByteCount);
const size_t FloatCount = Header.PixelByteCount / sizeof(float);
Texture.Pixels.resize(FloatCount);
Stream.read(reinterpret_cast<char *>(Texture.Pixels.data()),
static_cast<std::streamsize>(Texture.Pixels.size()));
static_cast<std::streamsize>(Header.PixelByteCount));
if (!Stream.good()) {
return std::nullopt;
}

if (!Texture.IsValid()) {
A_CORE_WARN("CookedTextureAsset: decoded invalid texture payload from '{}'",
A_CORE_WARN("CookedTextureAsset: decoded invalid HDR payload from '{}'",
Path.string());
return std::nullopt;
}
Expand Down
14 changes: 13 additions & 1 deletion Axiom/Assets/CookedTextureAsset.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@

namespace Axiom::Assets {

constexpr uint32_t kCookedTextureFormatVersion = 1;
constexpr uint32_t kCookedTextureFormatVersion = 2;

enum class CookedTexturePixelFormat : uint32_t {
RGBA8 = 0,
RGBA32F = 1,
};

bool SaveCookedTextureAsset(const std::filesystem::path &Path,
const TextureSourceData &Texture, AssetId Asset);

std::optional<TextureSourceData>
LoadCookedTextureAsset(const std::filesystem::path &Path);

bool SaveCookedHDRTextureAsset(const std::filesystem::path &Path,
const HDRTextureSourceData &Texture,
AssetId Asset);

std::optional<HDRTextureSourceData>
LoadCookedHDRTextureAsset(const std::filesystem::path &Path);

} // namespace Axiom::Assets
Loading
Loading