sm64coopdx/data/dynos_bin_tex.cpp
djoslin0 81af37eef6
Some checks are pending
Build coop / build-linux (push) Waiting to run
Build coop / build-steamos (push) Waiting to run
Build coop / build-windows-opengl (push) Waiting to run
Build coop / build-windows-directx (push) Waiting to run
Build coop / build-macos-arm (push) Waiting to run
Build coop / build-macos-intel (push) Waiting to run
Regenerate DynOS assets when source files are modified (#873)
Previously to recompile DynOS assets you had to remove the
bin/lvl/bhv/tex/col/etc files. Now the game will compare the
last-modified-timestamps of the generated/compiled assets vs
the other files that are within those directories. If the presumed-
source files have a later modified timestamp DynOS will regenerate
those assets.

While this results in scanning the attributes of files more, it
also prevents parsing files unnecessarily. Previously actors
would always parse their source files and build up GfxData
unnecessarily.

---------

Co-authored-by: MysterD <myster@d>
2025-07-02 23:24:35 +10:00

487 lines
16 KiB
C++

#include "dynos.cpp.h"
extern "C" {
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb/stb_image_write.h"
}
///////////
// Utils //
///////////
static bool FileTypeExists(SysPath& aFolder, const char* fileType) {
DIR *_Dir = opendir(aFolder.c_str());
if (!_Dir) { return false; }
int fileTypeLen = strlen(fileType);
struct dirent *_Ent = NULL;
while ((_Ent = readdir(_Dir)) != NULL) {
int nameLen = strlen(_Ent->d_name);
if (nameLen > fileTypeLen && !strcmp(&_Ent->d_name[nameLen - fileTypeLen], fileType)) {
closedir(_Dir);
return true;
}
}
closedir(_Dir);
return false;
}
static TexData* LoadTextureFromFile(GfxData *aGfxData, const char* aFile) {
// Image file
SysPath _Filename;
int fileNameLen = strlen(aFile);
if (aGfxData->mPackFolder.length() == 0) {
_Filename = aFile;
} else if (fileNameLen > 4 && !strcmp(&aFile[fileNameLen - 4], ".png")) {
_Filename = fstring("%s/%s", aGfxData->mPackFolder.c_str(), aFile);
} else {
_Filename = fstring("%s/%s.png", aGfxData->mPackFolder.c_str(), aFile);
}
FILE *_File = fopen(_Filename.c_str(), "rb");
// Check as if we're an Actor.
if (!_File) {
SysPath _ActorFilename = "";
const char* _SubString = strchr(aFile, '/'); // Remove the "actors/"
if (_SubString && *_SubString) {
_SubString++;
_ActorFilename = fstring("%s/%s.png", aGfxData->mPackFolder.c_str(), _SubString);
_File = fopen(_ActorFilename.c_str(), "rb");
}
// The file does not exist in either spot!
if (!_File) {
PrintDataError(" ERROR: Unable to open file at \"%s\" or \"%s\"", _Filename.c_str(), _ActorFilename.c_str());
return NULL;
}
}
// Texture data
fseek(_File, 0, SEEK_END);
TexData* _Texture = New<TexData>();
_Texture->mPngData.Resize(ftell(_File)); rewind(_File);
fread(_Texture->mPngData.begin(), sizeof(u8), _Texture->mPngData.Count(), _File);
fclose(_File);
return _Texture;
}
void DynOS_Tex_ConvertTextureDataToPng(GfxData *aGfxData, TexData* aTexture) {
// Convert to RGBA32
const u8 *_Palette = (aGfxData->mGfxContext.mCurrentPalette ? aGfxData->mGfxContext.mCurrentPalette->mData->mRawData.begin() : NULL);
u8 *_Buffer = DynOS_Tex_ConvertToRGBA32(aTexture->mRawData.begin(), aTexture->mRawData.Count(), aTexture->mRawFormat, aTexture->mRawSize, _Palette);
if (_Buffer == NULL) {
PrintDataError(" ERROR: Unknown texture format");
return;
}
// Convert to PNG
s32 _PngLength = 0;
u8 *_PngData = stbi_write_png_to_mem(_Buffer, 0, aTexture->mRawWidth, aTexture->mRawHeight, 4, &_PngLength);
if (!_PngData || !_PngLength) {
PrintDataError(" ERROR: Cannot convert texture to PNG");
return;
}
aTexture->mPngData = Array<u8>(_PngData, _PngData + _PngLength);
Delete(_PngData);
}
/////////////
// Parsing //
/////////////
DataNode<TexData>* DynOS_Tex_Parse(GfxData* aGfxData, DataNode<TexData>* aNode) {
if (aNode->mData) return aNode;
// Check tokens Count
if (aNode->mTokens.Count() < 1) {
PrintDataError(" ERROR: %s: not enough data", aNode->mName.begin());
return aNode;
}
// #include"[texture].inc.c"
s32 i0 = aNode->mTokens[0].Find("#include");
if (i0 != -1) {
s32 i1 = aNode->mTokens[0].Find(".inc.c");
if (i1 == -1) {
if (strstr(aNode->mName.begin(), "_pal_") == NULL) {
PrintDataError(" ERROR: %s: missing .inc.c in String %s", aNode->mName.begin(), aNode->mTokens[0].begin());
} else {
// hack for pal textures to be "found"
TexData* _Texture = New<TexData>();
aNode->mData = _Texture;
aNode->mLoadIndex = aGfxData->mLoadIndex++;
}
return aNode;
}
// Filename
String _Filename = aNode->mTokens[0].SubString(i0 + 9, i1 - i0 - 9);
aNode->mData = LoadTextureFromFile(aGfxData, _Filename.begin());
aNode->mLoadIndex = aGfxData->mLoadIndex++;
return aNode;
}
// double quoted String
s32 dq0 = aNode->mTokens[0].Find('\"');
if (dq0 != -1) {
s32 dq1 = aNode->mTokens[0].Find('\"', dq0 + 1);
if (dq1 == -1) {
PrintDataError(" ERROR: %s: missing second quote in String %s", aNode->mName.begin(), aNode->mTokens[0].begin());
return aNode;
}
// Filename
String _Filename = aNode->mTokens[0].SubString(dq0 + 1, dq1 - dq0 - 1);
aNode->mData = LoadTextureFromFile(aGfxData, _Filename.begin());
aNode->mLoadIndex = aGfxData->mLoadIndex++;
return aNode;
}
// Stream of bytes
aNode->mData = New<TexData>();
aNode->mData->mRawWidth = -1; // Unknown for now, will be set later
aNode->mData->mRawHeight = -1; // Unknown for now, will be set later
aNode->mData->mRawFormat = -1; // Unknown for now, will be set later
aNode->mData->mRawSize = -1; // Unknown for now, will be set later
aNode->mData->mRawData.Resize(aNode->mTokens.Count());
for (u64 j = 0; j != aNode->mTokens.Count(); ++j) {
aNode->mData->mRawData[j] = aNode->mTokens[j].ParseInt();
}
aNode->mLoadIndex = aGfxData->mLoadIndex++;
return aNode;
}
/////////////
// Writing //
/////////////
void DynOS_Tex_Write(BinFile* aFile, GfxData* aGfxData, DataNode<TexData> *aNode) {
if (!aNode->mData) return;
// Header
aFile->Write<u8>(DATA_TYPE_TEXTURE);
aNode->mName.Write(aFile);
// Data
// Look for texture duplicates
// If that's the case, store the name of the texture node instead of the whole PNG data
// (Don't bother to look for duplicates if there is no data to write)
if (!aNode->mData->mPngData.Empty()) {
for (const auto& _Node : aGfxData->mTextures) {
if (_Node->mLoadIndex < aNode->mLoadIndex && // Check load order: duplicates should reference only an already loaded node
_Node->mData != NULL && // Check node data
aNode->mData->mPngData.Count() == _Node->mData->mPngData.Count() && // Check PNG data lengths
memcmp(aNode->mData->mPngData.begin(), _Node->mData->mPngData.begin(), aNode->mData->mPngData.Count()) == 0) // Check PNG data content
{
aFile->Write<u32>(TEX_REF_CODE);
_Node->mName.Write(aFile);
return;
}
}
}
aNode->mData->mPngData.Write(aFile);
}
static bool DynOS_Tex_WriteBinary(GfxData* aGfxData, const SysPath &aOutputFilename, String& aName, TexData* aTexData, bool aRawTexture) {
BinFile *_File = BinFile::OpenW(aOutputFilename.c_str());
if (!_File) {
PrintDataError(" ERROR: Unable to create file \"%s\"", aOutputFilename.c_str());
return false;
}
if (!aRawTexture) {
// Write png-texture
// Header
_File->Write<u8>(DATA_TYPE_TEXTURE);
aName.Write(_File);
// Data
aTexData->mPngData.Write(_File);
BinFile::Close(_File);
return true;
}
// Write raw-texture
// Header
_File->Write<u8>(DATA_TYPE_TEXTURE_RAW);
aName.Write(_File);
// load
u8 *_RawData = stbi_load_from_memory(aTexData->mPngData.begin(), aTexData->mPngData.Count(), &aTexData->mRawWidth, &aTexData->mRawHeight, NULL, 4);
aTexData->mRawFormat = G_IM_FMT_RGBA;
aTexData->mRawSize = G_IM_SIZ_32b;
aTexData->mRawData = Array<u8>(_RawData, _RawData + (aTexData->mRawWidth * aTexData->mRawHeight * 4));
free(_RawData);
// Data
_File->Write<s32>(aTexData->mRawFormat);
_File->Write<s32>(aTexData->mRawSize);
_File->Write<s32>(aTexData->mRawWidth);
_File->Write<s32>(aTexData->mRawHeight);
aTexData->mRawData.Write(_File);
BinFile::Close(_File);
return true;
}
/////////////
// Reading //
/////////////
DataNode<TexData>* DynOS_Tex_Load(BinFile *aFile, GfxData *aGfxData) {
if (!aFile || !aGfxData) { return NULL; }
DataNode<TexData> *_Node = New<DataNode<TexData>>();
// Name
_Node->mName.Read(aFile);
// Data
_Node->mData = New<TexData>();
_Node->mData->mUploaded = false;
// Check for the texture ref magic
s32 _FileOffset = aFile->Offset();
u32 _TexRefCode = aFile->Read<u32>();
if (_TexRefCode == TEX_REF_CODE) {
// That's a duplicate, find the original node and copy its content
String _NodeName; _NodeName.Read(aFile);
for (const auto& _LoadedNode : aGfxData->mTextures) {
if (_LoadedNode->mName == _NodeName) {
_Node->mData->mPngData = _LoadedNode->mData->mPngData;
_Node->mData->mRawData = _LoadedNode->mData->mRawData;
_Node->mData->mRawWidth = _LoadedNode->mData->mRawWidth;
_Node->mData->mRawHeight = _LoadedNode->mData->mRawHeight;
_Node->mData->mRawFormat = _LoadedNode->mData->mRawFormat;
_Node->mData->mRawSize = _LoadedNode->mData->mRawSize;
break;
}
}
} else {
aFile->SetOffset(_FileOffset);
_Node->mData->mPngData.Read(aFile);
if (!_Node->mData->mPngData.Empty()) {
u8 *_RawData = stbi_load_from_memory(_Node->mData->mPngData.begin(), _Node->mData->mPngData.Count(), &_Node->mData->mRawWidth, &_Node->mData->mRawHeight, NULL, 4);
_Node->mData->mRawFormat = G_IM_FMT_RGBA;
_Node->mData->mRawSize = G_IM_SIZ_32b;
_Node->mData->mRawData = Array<u8>(_RawData, _RawData + (_Node->mData->mRawWidth * _Node->mData->mRawHeight * 4));
free(_RawData);
} else { // Probably a palette
_Node->mData->mRawData = Array<u8>();
_Node->mData->mRawWidth = 0;
_Node->mData->mRawHeight = 0;
_Node->mData->mRawFormat = 0;
_Node->mData->mRawSize = 0;
}
}
// Append
if (aGfxData) {
aGfxData->mTextures.Add(_Node);
}
return _Node;
}
DataNode<TexData>* DynOS_Tex_LoadFromBinary(const SysPath &aPackFolder, const SysPath &aFilename, const char *aTexName, bool aAddToPack) {
// Look for pack in cache
PackData* _Pack = DynOS_Pack_GetFromPath(aPackFolder);
// Look for tex in pack
if (_Pack) {
auto _Tex = DynOS_Pack_GetTex(_Pack, aTexName);
if (_Tex != NULL) {
return _Tex;
}
}
// Load data from binary file
DataNode<TexData>* _TexNode = NULL;
BinFile *_File = BinFile::OpenR(aFilename.c_str());
if (!_File) { return NULL; }
u8 type = _File->Read<u8>();
if (type == DATA_TYPE_TEXTURE) {
// load png-texture
_TexNode = New<DataNode<TexData>>();
_TexNode->mData = New<TexData>();
_TexNode->mName.Read(_File);
_TexNode->mData->mPngData.Read(_File);
BinFile::Close(_File);
if (aAddToPack) {
if (!_Pack) { _Pack = DynOS_Pack_Add(aPackFolder); }
DynOS_Pack_AddTex(_Pack, _TexNode);
}
return _TexNode;
} else if (type != DATA_TYPE_TEXTURE_RAW) {
BinFile::Close(_File);
return NULL;
}
// load raw-texture
_TexNode = New<DataNode<TexData>>();
_TexNode->mData = New<TexData>();
_TexNode->mName.Read(_File);
_TexNode->mData->mRawFormat = _File->Read<s32>();
_TexNode->mData->mRawSize = _File->Read<s32>();
_TexNode->mData->mRawWidth = _File->Read<s32>();
_TexNode->mData->mRawHeight = _File->Read<s32>();
_TexNode->mData->mRawData.Read(_File);
BinFile::Close(_File);
if (aAddToPack) {
if (!_Pack) { _Pack = DynOS_Pack_Add(aPackFolder); }
DynOS_Pack_AddTex(_Pack, _TexNode);
}
return _TexNode;
}
//////////////
// Generate //
//////////////
static bool is_level_number_png(SysPath& aPath) {
// normalize
String path = aPath.c_str();
char* p = path.begin();
while (*p != '\0') {
if (*p == '\\') {
*p = '/';
}
break;
}
p = path.begin();
// compare 'levels/'
s16 levelsLength = strlen("levels/");
if (strncmp(p, "levels/", levelsLength)) {
return false;
}
// skip past level name
p += levelsLength;
while (*p != '\0') {
if (*p == '/') { break; }
p++;
}
if (*p != '/') { return false; }
p++;
return (*p >= '0' && *p <= '9');
}
static void DynOS_Tex_GeneratePack_Recursive(const SysPath &aPackFolder, SysPath &aOutputFolder, SysPath& aRelativePath, SysPath& aPrefix, GfxData *aGfxData, bool aAllowCustomTextures) {
SysPath _DirPath = fstring("%s/%s", aPackFolder.c_str(), aRelativePath.c_str());
// skip generation if any .c files exist, and it isn't levels/xxx/NUMBER
bool containsC = FileTypeExists(_DirPath, ".c");
DIR *_PackDir = opendir(_DirPath.c_str());
if (!_PackDir) { return; }
struct dirent *_PackEnt = NULL;
while ((_PackEnt = readdir(_PackDir)) != NULL) {
// Skip . and ..
if (SysPath(_PackEnt->d_name) == ".") continue;
if (SysPath(_PackEnt->d_name) == "..") continue;
SysPath _Path = fstring("%s%s", _DirPath.c_str(), _PackEnt->d_name);
// Recurse through subfolders
if (fs_sys_dir_exists(_Path.c_str())) {
SysPath _NextPath = fstring("%s%s/", aRelativePath.c_str(), _PackEnt->d_name);
SysPath _Prefix = fstring("%s.", _PackEnt->d_name);
DynOS_Tex_GeneratePack_Recursive(aPackFolder, aOutputFolder, _NextPath, _Prefix, aGfxData, aAllowCustomTextures);
continue;
}
size_t nameLen = strlen(_PackEnt->d_name);
if (nameLen < 4) continue;
// skip files that don't end in '.png'
if (strcmp(&_PackEnt->d_name[nameLen - 4], ".png")) {
continue;
}
// read the file
aGfxData->mModelIdentifier++;
TexData* _TexData = LoadTextureFromFile(aGfxData, _Path.c_str());
if (_TexData == NULL) {
PrintDataError("Error reading texture from file: %s", _Path.c_str());
continue;
}
SysPath _RelativePath = fstring("%s%s", aRelativePath.c_str(), _PackEnt->d_name);
if (containsC && !is_level_number_png(_RelativePath)) {
// Don't forgot to free the texture data we've read.
Delete<TexData>(_TexData);
continue;
}
// write the file
String _BaseName;
const char* _OverrideName = DynOS_Builtin_Tex_GetNameFromFileName(_RelativePath.c_str());
if (_OverrideName) {
_BaseName = _OverrideName;
} else {
_BaseName = _PackEnt->d_name;
_BaseName = _BaseName.SubString(0, nameLen - 4);
}
// if we aren't overriding a texture, only generate textures in the output directory
SysPath _OutputFolder = fstring("%s/", aOutputFolder.c_str());
if (_OverrideName == NULL && (!aAllowCustomTextures || strcmp(_DirPath.c_str(), _OutputFolder.c_str()))) {
// Don't forgot to free the texture data we've read.
Delete<TexData>(_TexData);
continue;
}
SysPath _OutputPath = fstring("%s/%s.tex", aOutputFolder.c_str(), _BaseName.begin());
// skip files that have already been generated
if (DynOS_GenFileExistsAndIsNewerThanFile(_OutputPath, _Path)) {
Delete<TexData>(_TexData);
continue;
}
// create output dir if it doesn't exist
if (!fs_sys_dir_exists(aOutputFolder.c_str())) {
fs_sys_mkdir(aOutputFolder.c_str());
}
DynOS_Tex_WriteBinary(aGfxData, _OutputPath, _BaseName, _TexData, (_OverrideName != NULL));
// Don't forgot to free the texture data we've read.
Delete<TexData>(_TexData);
}
closedir(_PackDir);
}
void DynOS_Tex_GeneratePack(const SysPath &aPackFolder, SysPath &aOutputFolder, bool aAllowCustomTextures) {
Print("Processing textures: \"%s\"", aPackFolder.c_str());
if (!DynOS_ShouldGeneratePack2Ext(aPackFolder, ".tex", ".png")) {
return;
}
GfxData *_GfxData = New<GfxData>();
_GfxData->mModelIdentifier = 0;
SysPath _Empty = "";
DynOS_Tex_GeneratePack_Recursive(aPackFolder, aOutputFolder, _Empty, _Empty, _GfxData, aAllowCustomTextures);
DynOS_Gfx_Free(_GfxData);
}