Skip to content

Commit a283091

Browse files
committed
feat: add functions to stack and load DDS textures
The new functions in `core_tex_manipulation` can be used to construct a handle object that can perform light image manipulation, currently just stacking of DDS textures of the same extents and formats into a layered DDS texture. Image loading has gained the ability to load DDS files, optionally compressed with Zstandard as `.dds.zst`.
1 parent 6197c08 commit a283091

8 files changed

Lines changed: 352 additions & 5 deletions

File tree

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,16 @@ set(SIMPLEGRAPHIC_SOURCES
3636
"engine/common/memtrak3.h"
3737
"engine/common/streams.cpp"
3838
"engine/common/streams.h"
39+
"engine/core/core_compress.cpp"
40+
"engine/core/core_compress.h"
3941
"engine/core/core_config.cpp"
4042
"engine/core/core_config.h"
4143
"engine/core/core_image.cpp"
4244
"engine/core/core_image.h"
4345
"engine/core/core_main.cpp"
4446
"engine/core/core_main.h"
47+
"engine/core/core_tex_manipulation.cpp"
48+
"engine/core/core_tex_manipulation.h"
4549
"engine/core/core_video.cpp"
4650
"engine/core/core_video.h"
4751
"engine/render/r_font.cpp"

engine/core/core_compress.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#include "core_compress.h"
2+
3+
std::optional<std::vector<char>> CompressZstandard(gsl::span<const std::byte> src, std::optional<int> level)
4+
{
5+
if (!level)
6+
level = ZSTD_defaultCLevel();
7+
std::vector<char> dst(ZSTD_compressBound(src.size()));
8+
size_t rc = ZSTD_compress(dst.data(), dst.size(), src.data(), src.size(), *level);
9+
if (ZSTD_isError(rc))
10+
return {};
11+
dst.resize(rc);
12+
return dst;
13+
}
14+
15+
std::optional<std::vector<char>> DecompressZstandard(gsl::span<const std::byte> src)
16+
{
17+
const size_t buffOutSize = ZSTD_DStreamOutSize();
18+
std::vector<char> buffOut(buffOutSize);
19+
20+
std::vector<char> dst;
21+
dst.reserve(1 << 20);
22+
23+
std::shared_ptr<ZSTD_DCtx> dctx(ZSTD_createDCtx(), &ZSTD_freeDCtx);
24+
ZSTD_inBuffer input = { src.data(), src.size(), 0 };
25+
while (input.pos < input.size) {
26+
ZSTD_outBuffer output = { buffOut.data(), buffOut.size(), 0 };
27+
const size_t rc = ZSTD_decompressStream(dctx.get(), &output, &input);
28+
if (ZSTD_isError(rc)) {
29+
return {};
30+
}
31+
auto oldSize = dst.size();
32+
auto newSize = oldSize + output.pos;
33+
if (newSize > dst.capacity()) {
34+
dst.reserve((size_t)(newSize * 1.5));
35+
}
36+
dst.resize(newSize);
37+
memcpy(dst.data() + oldSize, output.dst, output.pos);
38+
}
39+
40+
dst.shrink_to_fit();
41+
return dst;
42+
}

engine/core/core_compress.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#pragma once
2+
3+
#include <memory>
4+
#include <optional>
5+
#include <vector>
6+
7+
#include <gsl/span>
8+
#include <zstd.h>
9+
10+
std::optional<std::vector<char>> CompressZstandard(gsl::span<const std::byte> src, std::optional<int> level = {});
11+
12+
std::optional<std::vector<char>> DecompressZstandard(gsl::span<const std::byte> src);

engine/core/core_image.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "common.h"
88

99
#include "core_image.h"
10+
#include "core_compress.h"
1011

1112
#define STB_IMAGE_IMPLEMENTATION
1213
#include "stb_image.h"
@@ -18,6 +19,7 @@
1819
#include "stb_image_write.h"
1920

2021
#include <algorithm>
22+
#include <filesystem>
2123
#include <thread>
2224
#include <vector>
2325

@@ -26,6 +28,7 @@
2628
#include <jxl/resizable_parallel_runner_cxx.h>
2729

2830
#include <gli/gli.hpp>
31+
#include <gsl/span>
2932

3033
// =========
3134
// Raw Image
@@ -97,6 +100,18 @@ image_c* image_c::LoaderForFile(IConsole* conHnd, const char* fileName)
97100
conHnd->Warning("'%s' doesn't exist or cannot be opened", fileName);
98101
return NULL;
99102
}
103+
104+
// Detect first by extension, as decompressing could be expensive.
105+
auto p = std::filesystem::u8path(fileName);
106+
if (p.extension() == ".zst") {
107+
auto inner = p.filename();
108+
inner.replace_extension();
109+
if (inner.extension() == ".dds")
110+
return new dds_c(conHnd);
111+
}
112+
if (p.extension() == ".dds")
113+
return new dds_c(conHnd);
114+
100115
// Attempt to detect image file type from first 12 bytes of file
101116
byte dat[12];
102117
if (in.Read(dat, 12)) {
@@ -115,6 +130,9 @@ image_c* image_c::LoaderForFile(IConsole* conHnd, const char* fileName)
115130
} else if (auto sig = JxlSignatureCheck(dat, 12); sig == JXL_SIG_CODESTREAM || sig == JXL_SIG_CONTAINER) {
116131
// JPEG XL
117132
return new jpeg_xl_c(conHnd);
133+
} else if (*(dword*)dat == 0x20534444) {
134+
// D D S 0x20
135+
return new dds_c(conHnd);
118136
} else if ((dat[1] == 0 && (dat[2] == 2 || dat[2] == 3 || dat[2] == 10 || dat[2] == 11)) || (dat[1] == 1 && (dat[2] == 1 || dat[2] == 9))) {
119137
// Detect all valid image types (whether supported or not)
120138
return new targa_c(conHnd);
@@ -525,3 +543,35 @@ bool jpeg_xl_c::Save(const char* fileName)
525543
// Yeah, nah.
526544
return false;
527545
}
546+
547+
bool dds_c::Load(const char* fileName, std::optional<size_callback_t> sizeCallback)
548+
{
549+
auto p = std::filesystem::u8path(fileName);
550+
// Open file
551+
fileInputStream_c in;
552+
if (in.FileOpen(fileName, true))
553+
return true;
554+
555+
std::vector<byte> fileData(in.GetLen());
556+
if (in.Read(fileData.data(), fileData.size()))
557+
return true;
558+
559+
if (p.extension() == ".zst" || fileData.size() >= 4 && *(uint32_t*)fileData.data() == 0xFD2FB528) {
560+
auto ret = DecompressZstandard(as_bytes(gsl::span(fileData)));
561+
if (!ret.has_value())
562+
return true;
563+
fileData.assign(ret->data(), ret->data() + ret->size());
564+
}
565+
566+
tex = gli::texture2d_array(gli::load_dds((const char*)fileData.data(), fileData.size()));
567+
if (sizeCallback)
568+
(*sizeCallback)(tex.extent().x, tex.extent().y);
569+
570+
return false;
571+
}
572+
573+
bool dds_c::Save(const char* fileName)
574+
{
575+
// Nope.
576+
return true;
577+
}

engine/core/core_image.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,11 @@ class gif_c : public image_c {
104104
bool Load(const char* fileName, std::optional<size_callback_t> sizeCallback = {}) override;
105105
bool Save(const char* fileName) override;
106106
};
107+
108+
// DDS Image
109+
class dds_c : public image_c {
110+
public:
111+
dds_c(IConsole* conHnd) : image_c(conHnd) {}
112+
bool Load(const char* fileName, std::optional<size_callback_t> sizeCallback = {}) override;
113+
bool Save(const char* fileName) override;
114+
};
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#include "core_tex_manipulation.h"
2+
#include "core_compress.h"
3+
4+
#include <filesystem>
5+
#include <fstream>
6+
#include <string_view>
7+
#include <unordered_map>
8+
9+
#include "ui_local.h"
10+
11+
#include <gli/load_dds.hpp>
12+
#include <gli/save_dds.hpp>
13+
#include <gsl/span>
14+
#include <zstd.h>
15+
16+
using namespace std::string_view_literals;
17+
18+
Texture_c::Texture_c()
19+
{
20+
}
21+
22+
static std::unordered_map<std::string_view, gli::format> g_formatStrToId{
23+
{"RGB"sv, gli::format::FORMAT_RGB8_UNORM_PACK8},
24+
{"RGBA"sv, gli::format::FORMAT_RGBA8_UNORM_PACK8},
25+
{"BC1"sv, gli::format::FORMAT_RGBA_DXT1_UNORM_BLOCK8},
26+
{"BC7"sv, gli::format::FORMAT_RGBA_BP_UNORM_BLOCK16},
27+
};
28+
29+
static std::unordered_map<gli::format, std::string_view> g_formatIdToStr{
30+
{gli::format::FORMAT_RGB8_UNORM_PACK8, "RGB"sv},
31+
{gli::format::FORMAT_RGBA8_UNORM_PACK8, "RGBA"sv},
32+
{gli::format::FORMAT_RGBA_DXT1_UNORM_BLOCK8, "BC1"sv},
33+
{gli::format::FORMAT_RGBA_BP_UNORM_BLOCK16, "BC7"sv},
34+
};
35+
36+
bool Texture_c::Allocate(gli::format format, int width, int height, int layerCount, int mipCount)
37+
{
38+
auto extent = gli::texture2d_array::extent_type(width, height);
39+
tex = gli::texture2d_array(format, extent, layerCount, mipCount);
40+
return true;
41+
}
42+
43+
bool Texture_c::Allocate(std::string_view format, int width, int height, int layerCount, int mipCount)
44+
{
45+
gli::format formatId = gli::format::FORMAT_UNDEFINED;
46+
if (auto I = g_formatStrToId.find(format); I != g_formatStrToId.end())
47+
formatId = I->second;
48+
else
49+
return false;
50+
51+
return Allocate(formatId, width, height, layerCount, mipCount);
52+
}
53+
54+
bool Texture_c::Load(const char* fileName)
55+
{
56+
auto path = std::filesystem::u8path(fileName);
57+
std::vector<char> fileData;
58+
{
59+
std::ifstream is(path, std::ios::binary);
60+
if (!is.is_open())
61+
return false;
62+
is.seekg(0, std::ios::end);
63+
fileData.resize(is.tellg());
64+
is.seekg(0, std::ios::beg);
65+
is.read(fileData.data(), fileData.size());
66+
}
67+
68+
auto fileTex = gli::load_dds(fileData.data(), fileData.size());
69+
if (fileTex.faces() != 1)
70+
return false;
71+
72+
tex = gli::texture2d_array(fileTex);
73+
return !tex.empty();
74+
}
75+
76+
77+
78+
bool Texture_c::Save(const char* fileName)
79+
{
80+
if (tex.empty())
81+
return false;
82+
83+
bool wantDds = false;
84+
bool wantZstd = false;
85+
auto path = std::filesystem::u8path(fileName);
86+
auto subPath = path;
87+
if (subPath.extension() == ".zst") {
88+
wantZstd = true;
89+
subPath = subPath.replace_extension();
90+
}
91+
92+
if (subPath.extension() == ".dds") {
93+
wantDds = true;
94+
subPath = subPath.replace_extension();
95+
}
96+
97+
if (!wantDds)
98+
return false;
99+
100+
std::vector<char> fileData;
101+
if (!gli::save_dds(tex, fileData))
102+
return false;
103+
104+
if (wantZstd) {
105+
auto res = CompressZstandard(as_bytes(gsl::span(fileData)));
106+
if (!res.has_value())
107+
return false;
108+
fileData = std::move(res.value());
109+
}
110+
111+
std::ofstream os(path, std::ios::binary);
112+
if (!os.is_open())
113+
return false;
114+
115+
os.write(fileData.data(), fileData.size());
116+
return !!os;
117+
}
118+
119+
TextureInfo_s Texture_c::Info() const
120+
{
121+
TextureInfo_s ret = {};
122+
ret.formatId = tex.format();
123+
if (auto I = g_formatIdToStr.find(ret.formatId); I != g_formatIdToStr.end())
124+
ret.formatStr = I->second;
125+
else
126+
ret.formatStr = "<unknown>"sv;
127+
ret.width = tex.extent().x;
128+
ret.height = tex.extent().y;
129+
ret.layerCount = (int)tex.layers();
130+
ret.mipCount = (int)tex.levels();
131+
return ret;
132+
}
133+
134+
bool Texture_c::IsValid() const
135+
{
136+
return !tex.empty();
137+
}
138+
139+
//bool Texture_c::SetLayer(Texture_c& srcTex, int targetLayer)
140+
//{
141+
//#if 0
142+
// if (srcTex.tex.extent() != tex.extent())
143+
// return false;
144+
// if (srcTex.tex.layers() != dstTex.layers())
145+
// tex.copy(srcTex, 0, 0, )
146+
//#endif
147+
// return false;
148+
//}
149+
//
150+
//bool Texture_c::CopyImage(Texture_c& srcTex, int targetX, int targetY)
151+
//{
152+
// return false;
153+
//}
154+
//
155+
//bool Texture_c::Transcode(gli::format format)
156+
//{
157+
// return false;
158+
//}
159+
//
160+
//bool Texture_c::GenerateMipmaps()
161+
//{
162+
// return false;
163+
//}
164+
165+
bool Texture_c::StackTextures(std::vector<Texture_c> textures)
166+
{
167+
if (textures.empty())
168+
return false;
169+
170+
const auto dstFormat = textures.front().tex.format();
171+
const auto dstExtent = textures.front().tex.extent();
172+
const auto dstLevels = textures.front().tex.levels();
173+
174+
// Ensure that all the source textures are flat and compatible.
175+
for (auto& src : textures)
176+
if (src.tex.layers() != 1 ||
177+
src.tex.faces() != 1 ||
178+
src.tex.extent() != dstExtent ||
179+
src.tex.format() != dstFormat ||
180+
src.tex.levels() != dstLevels)
181+
{
182+
return false;
183+
}
184+
185+
tex = gli::texture2d_array(dstFormat, dstExtent, textures.size(), dstLevels);
186+
for (size_t layerIdx = 0; layerIdx < textures.size(); ++layerIdx) {
187+
auto& src = textures[layerIdx].tex;
188+
for (size_t levelIdx = 0; levelIdx < dstLevels; ++levelIdx)
189+
tex.copy(src, 0, 0, levelIdx, layerIdx, 0, levelIdx);
190+
}
191+
192+
return true;
193+
}

0 commit comments

Comments
 (0)