ASTC 6x6 HDR/6x6 HDR intermediate update

This commit is contained in:
Richard Geldreich
2025-01-21 11:01:08 -05:00
parent ae0dcd6f62
commit 1ba75b4e34
49 changed files with 26835 additions and 6776 deletions

View File

@@ -21,7 +21,9 @@
#include "jpgd.h"
#include "pvpngreader.h"
#include "basisu_opencl.h"
#include "basisu_astc_hdr_enc.h"
#include "basisu_uastc_hdr_4x4_enc.h"
#include "basisu_astc_hdr_6x6_enc.h"
#include <vector>
#ifndef TINYEXR_USE_ZFP
@@ -50,10 +52,13 @@ namespace basisu
{
uint64_t interval_timer::g_init_ticks, interval_timer::g_freq;
double interval_timer::g_timer_freq;
#if BASISU_SUPPORT_SSE
bool g_cpu_supports_sse41;
#endif
fast_linear_to_srgb g_fast_linear_to_srgb;
uint8_t g_hamming_dist[256] =
{
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
@@ -178,7 +183,7 @@ namespace basisu
bool g_library_initialized;
std::mutex g_encoder_init_mutex;
// Encoder library initialization (just call once at startup)
bool basisu_encoder_init(bool use_opencl, bool opencl_force_serialization)
{
@@ -188,7 +193,7 @@ namespace basisu
return true;
detect_sse41();
basist::basisu_transcoder_init();
pack_etc1_solid_color_init();
//uastc_init();
@@ -204,6 +209,7 @@ namespace basisu
astc_hdr_enc_init();
basist::bc6h_enc_init();
astc_6x6_hdr::global_init();
g_library_initialized = true;
return true;
@@ -218,15 +224,40 @@ namespace basisu
void error_vprintf(const char* pFmt, va_list args)
{
char buf[8192];
const uint32_t BUF_SIZE = 256;
char buf[BUF_SIZE];
#ifdef _WIN32
vsprintf_s(buf, sizeof(buf), pFmt, args);
#else
vsnprintf(buf, sizeof(buf), pFmt, args);
#endif
va_list args_copy;
va_copy(args_copy, args);
int total_chars = vsnprintf(buf, sizeof(buf), pFmt, args_copy);
va_end(args_copy);
fprintf(stderr, "ERROR: %s", buf);
if (total_chars < 0)
{
assert(0);
return;
}
if (total_chars >= (int)BUF_SIZE)
{
basisu::vector<char> var_buf(total_chars + 1);
va_copy(args_copy, args);
int total_chars_retry = vsnprintf(var_buf.data(), var_buf.size(), pFmt, args_copy);
va_end(args_copy);
if (total_chars_retry < 0)
{
assert(0);
return;
}
fprintf(stderr, "ERROR: %s", var_buf.data());
}
else
{
fprintf(stderr, "ERROR: %s", buf);
}
}
void error_printf(const char *pFmt, ...)
@@ -237,6 +268,18 @@ namespace basisu
va_end(args);
}
#if defined(_WIN32)
void platform_sleep(uint32_t ms)
{
Sleep(ms);
}
#else
void platform_sleep(uint32_t ms)
{
// TODO
}
#endif
#if defined(_WIN32)
inline void query_counter(timer_ticks* pTicks)
{
@@ -334,6 +377,8 @@ namespace basisu
return ticks * g_timer_freq;
}
// Note this is linear<->sRGB, NOT REC709 which uses slightly different equations/transfer functions.
// However the gamuts/white points of REC709 and sRGB are the same.
float linear_to_srgb(float l)
{
assert(l >= 0.0f && l <= 1.0f);
@@ -342,7 +387,7 @@ namespace basisu
else
return saturate(1.055f * powf(l, 1.0f / 2.4f) - .055f);
}
float srgb_to_linear(float s)
{
assert(s >= 0.0f && s <= 1.0f);
@@ -430,6 +475,7 @@ namespace basisu
uint32_t width = 0, height = 0, num_chans = 0;
void* pImage = pv_png::load_png(pBuf, buf_size, 4, width, height, num_chans);
if (!pImage)
{
error_printf("pv_png::load_png failed while loading image \"%s\"\n", pFilename);
@@ -469,6 +515,26 @@ namespace basisu
return true;
}
bool load_jpg(const uint8_t* pBuf, size_t buf_size, image& img)
{
if (buf_size > INT_MAX)
{
assert(0);
return false;
}
int width = 0, height = 0, actual_comps = 0;
uint8_t* pImage_data = jpgd::decompress_jpeg_image_from_memory(pBuf, (int)buf_size, &width, &height, &actual_comps, 4, jpgd::jpeg_decoder::cFlagLinearChromaFiltering);
if (!pImage_data)
return false;
img.init(pImage_data, width, height, 4);
free(pImage_data);
return true;
}
bool load_image(const char* pFilename, image& img)
{
std::string ext(string_get_extension(std::string(pFilename)));
@@ -490,7 +556,7 @@ namespace basisu
return false;
}
static void convert_ldr_to_hdr_image(imagef &img, const image &ldr_img, bool ldr_srgb_to_linear)
static void convert_ldr_to_hdr_image(imagef &img, const image &ldr_img, bool ldr_srgb_to_linear, float linear_nit_multiplier = 1.0f, float ldr_black_bias = 0.0f)
{
img.resize(ldr_img.get_width(), ldr_img.get_height());
@@ -503,23 +569,41 @@ namespace basisu
vec4F& d = img(x, y);
if (ldr_srgb_to_linear)
{
// TODO: Multiply by 100-200 nits?
d[0] = srgb_to_linear(c[0] * (1.0f / 255.0f));
d[1] = srgb_to_linear(c[1] * (1.0f / 255.0f));
d[2] = srgb_to_linear(c[2] * (1.0f / 255.0f));
float r = (float)c[0];
float g = (float)c[1];
float b = (float)c[2];
if (ldr_black_bias > 0.0f)
{
// ASTC HDR is noticeably weaker dealing with blocks containing some pixels with components set to 0.
// Add a very slight bias less than .5 to avoid this difficulity. When the HDR image is mapped to SDR sRGB and rounded back to 8-bits, this bias will still result in zero.
// (FWIW, in reality, a physical monitor would be unlikely to have a perfectly zero black level.)
// This is purely optional and on most images it doesn't matter visually.
if (r == 0.0f)
r = ldr_black_bias;
if (g == 0.0f)
g = ldr_black_bias;
if (b == 0.0f)
b = ldr_black_bias;
}
// Compute how much linear light would be emitted by a SDR 80-100 nit monitor.
d[0] = srgb_to_linear(r * (1.0f / 255.0f)) * linear_nit_multiplier;
d[1] = srgb_to_linear(g * (1.0f / 255.0f)) * linear_nit_multiplier;
d[2] = srgb_to_linear(b * (1.0f / 255.0f)) * linear_nit_multiplier;
}
else
{
d[0] = c[0] * (1.0f / 255.0f);
d[1] = c[1] * (1.0f / 255.0f);
d[2] = c[2] * (1.0f / 255.0f);
d[0] = c[0] * (1.0f / 255.0f) * linear_nit_multiplier;
d[1] = c[1] * (1.0f / 255.0f) * linear_nit_multiplier;
d[2] = c[2] * (1.0f / 255.0f) * linear_nit_multiplier;
}
d[3] = c[3] * (1.0f / 255.0f);
}
}
}
bool load_image_hdr(const void* pMem, size_t mem_size, imagef& img, uint32_t width, uint32_t height, hdr_image_type img_type, bool ldr_srgb_to_linear)
bool load_image_hdr(const void* pMem, size_t mem_size, imagef& img, uint32_t width, uint32_t height, hdr_image_type img_type, bool ldr_srgb_to_linear, float linear_nit_multiplier, float ldr_black_bias)
{
if ((!pMem) || (!mem_size))
{
@@ -583,13 +667,22 @@ namespace basisu
break;
}
case hdr_image_type::cHITJPGImage:
{
image ldr_img;
if (!load_jpg(static_cast<const uint8_t*>(pMem), mem_size, ldr_img))
return false;
convert_ldr_to_hdr_image(img, ldr_img, ldr_srgb_to_linear, linear_nit_multiplier, ldr_black_bias);
break;
}
case hdr_image_type::cHITPNGImage:
{
image ldr_img;
if (!load_png(static_cast<const uint8_t *>(pMem), mem_size, ldr_img))
return false;
convert_ldr_to_hdr_image(img, ldr_img, ldr_srgb_to_linear);
convert_ldr_to_hdr_image(img, ldr_img, ldr_srgb_to_linear, linear_nit_multiplier, ldr_black_bias);
break;
}
case hdr_image_type::cHITEXRImage:
@@ -617,8 +710,21 @@ namespace basisu
return true;
}
bool is_image_filename_hdr(const char *pFilename)
{
std::string ext(string_get_extension(std::string(pFilename)));
if (ext.length() == 0)
return false;
const char* pExt = ext.c_str();
return ((strcasecmp(pExt, "hdr") == 0) || (strcasecmp(pExt, "exr") == 0));
}
bool load_image_hdr(const char* pFilename, imagef& img, bool ldr_srgb_to_linear)
// TODO: move parameters to struct, add a HDR clean flag to eliminate NaN's/Inf's
bool load_image_hdr(const char* pFilename, imagef& img, bool ldr_srgb_to_linear, float linear_nit_multiplier, float ldr_black_bias)
{
std::string ext(string_get_extension(std::string(pFilename)));
@@ -649,7 +755,7 @@ namespace basisu
if (!load_image(pFilename, ldr_img))
return false;
convert_ldr_to_hdr_image(img, ldr_img, ldr_srgb_to_linear);
convert_ldr_to_hdr_image(img, ldr_img, ldr_srgb_to_linear, linear_nit_multiplier, ldr_black_bias);
}
return true;
@@ -1014,7 +1120,7 @@ namespace basisu
return false;
}
if ((src_w == dst_w) && (src_h == dst_h))
if ((src_w == dst_w) && (src_h == dst_h) && (filter_scale == 1.0f))
{
dst = src;
return true;
@@ -1664,7 +1770,7 @@ namespace basisu
uint32_t a = max_index / num_syms, b = max_index % num_syms;
const uint32_t ofs = m_entries_picked.size();
const size_t ofs = m_entries_picked.size();
m_entries_picked.push_back(a);
m_entries_picked.push_back(b);
@@ -2014,6 +2120,34 @@ namespace basisu
m_psnr = m_rms ? (float)clamp<double>(log10(255.0 / m_rms) * 20.0f, 0.0f, 100.0f) : 100.0f;
}
void print_image_metrics(const image& a, const image& b)
{
image_metrics im;
im.calc(a, b, 0, 3);
im.print("RGB ");
im.calc(a, b, 0, 4);
im.print("RGBA ");
im.calc(a, b, 0, 1);
im.print("R ");
im.calc(a, b, 1, 1);
im.print("G ");
im.calc(a, b, 2, 1);
im.print("B ");
im.calc(a, b, 3, 1);
im.print("A ");
im.calc(a, b, 0, 0);
im.print("Y 709 ");
im.calc(a, b, 0, 0, true, true);
im.print("Y 601 ");
}
void fill_buffer_with_random_bytes(void *pBuf, size_t size, uint32_t seed)
{
rand r(seed);
@@ -2091,9 +2225,11 @@ namespace basisu
}
job_pool::job_pool(uint32_t num_threads) :
m_num_active_jobs(0),
m_kill_flag(false)
m_num_active_jobs(0)
{
m_kill_flag.store(false);
m_num_active_workers.store(0);
assert(num_threads >= 1U);
debug_printf("job_pool::job_pool: %u total threads\n", num_threads);
@@ -2112,11 +2248,23 @@ namespace basisu
debug_printf("job_pool::~job_pool\n");
// Notify all workers that they need to die right now.
m_kill_flag = true;
m_kill_flag.store(true);
m_has_work.notify_all();
// Wait for all workers to die.
#ifdef __EMSCRIPTEN__
for ( ; ; )
{
if (m_num_active_workers.load() <= 0)
break;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// At this point all worker threads should be exiting or exited.
// We could call detach(), but this seems to just call join() anyway.
#endif
// Wait for all worker threads to exit.
for (uint32_t i = 0; i < m_threads.size(); i++)
m_threads[i].join();
}
@@ -2169,13 +2317,26 @@ namespace basisu
}
// The queue is empty, now wait for all active jobs to finish up.
#ifndef __EMSCRIPTEN__
m_no_more_jobs.wait(lock, [this]{ return !m_num_active_jobs; } );
#else
// Avoid infinite blocking
for (; ; )
{
if (m_no_more_jobs.wait_for(lock, std::chrono::milliseconds(50), [this] { return !m_num_active_jobs; }))
{
break;
}
}
#endif
}
void job_pool::job_thread(uint32_t index)
{
BASISU_NOTE_UNUSED(index);
//debug_printf("job_pool::job_thread: starting %u\n", index);
m_num_active_workers.fetch_add(1);
while (true)
{
@@ -2211,6 +2372,8 @@ namespace basisu
m_no_more_jobs.notify_all();
}
m_num_active_workers.fetch_add(-1);
//debug_printf("job_pool::job_thread: exiting\n");
}
@@ -3325,7 +3488,7 @@ namespace basisu
return true;
}
bool write_exr(const char* pFilename, imagef& img, uint32_t n_chans, uint32_t flags)
bool write_exr(const char* pFilename, const imagef& img, uint32_t n_chans, uint32_t flags)
{
assert((n_chans == 1) || (n_chans == 3) || (n_chans == 4));
@@ -3494,11 +3657,14 @@ namespace basisu
// Very basic global Reinhard tone mapping, output converted to sRGB with no dithering, alpha is carried through unchanged.
// Only used for debugging/development.
void tonemap_image_reinhard(image &ldr_img, const imagef &hdr_img, float exposure)
void tonemap_image_reinhard(image &ldr_img, const imagef &hdr_img, float exposure, bool add_noise, bool per_component, bool luma_scaling)
{
uint32_t width = hdr_img.get_width(), height = hdr_img.get_height();
ldr_img.resize(width, height);
rand r;
r.seed(128);
for (uint32_t y = 0; y < height; y++)
{
@@ -3506,32 +3672,84 @@ namespace basisu
{
vec4F c(hdr_img(x, y));
for (uint32_t t = 0; t < 3; t++)
if (per_component)
{
if (c[t] <= 0.0f)
for (uint32_t t = 0; t < 3; t++)
{
c[t] = 0.0f;
if (c[t] <= 0.0f)
{
c[t] = 0.0f;
}
else
{
c[t] *= exposure;
c[t] = c[t] / (1.0f + c[t]);
}
}
else
}
else
{
c[0] *= exposure;
c[1] *= exposure;
c[2] *= exposure;
const float L = 0.2126f * c[0] + 0.7152f * c[1] + 0.0722f * c[2];
float Lmapped = 0.0f;
if (L > 0.0f)
{
c[t] *= exposure;
c[t] = c[t] / (1.0f + c[t]);
//Lmapped = L / (1.0f + L);
//Lmapped /= L;
Lmapped = 1.0f / (1.0f + L);
}
c[0] = c[0] * Lmapped;
c[1] = c[1] * Lmapped;
c[2] = c[2] * Lmapped;
if (luma_scaling)
{
// Keeps the ratio of r/g/b intact
float m = maximum(c[0], c[1], c[2]);
if (m > 1.0f)
{
c /= m;
}
}
}
c.clamp(0.0f, 1.0f);
c[0] = linear_to_srgb(c[0]) * 255.0f;
c[1] = linear_to_srgb(c[1]) * 255.0f;
c[2] = linear_to_srgb(c[2]) * 255.0f;
c[3] = c[3] * 255.0f;
color_rgba& o = ldr_img(x, y);
o[0] = (uint8_t)std::round(c[0]);
o[1] = (uint8_t)std::round(c[1]);
o[2] = (uint8_t)std::round(c[2]);
o[3] = (uint8_t)std::round(c[3]);
if (add_noise)
{
c[0] = linear_to_srgb(c[0]) * 255.0f;
c[1] = linear_to_srgb(c[1]) * 255.0f;
c[2] = linear_to_srgb(c[2]) * 255.0f;
const float NOISE_AMP = .5f;
c[0] += r.frand(-NOISE_AMP, NOISE_AMP);
c[1] += r.frand(-NOISE_AMP, NOISE_AMP);
c[2] += r.frand(-NOISE_AMP, NOISE_AMP);
c.clamp(0.0f, 255.0f);
o[0] = (uint8_t)fast_roundf_int(c[0]);
o[1] = (uint8_t)fast_roundf_int(c[1]);
o[2] = (uint8_t)fast_roundf_int(c[2]);
o[3] = (uint8_t)fast_roundf_int(c[3]);
}
else
{
o[0] = g_fast_linear_to_srgb.convert(c[0]);
o[1] = g_fast_linear_to_srgb.convert(c[1]);
o[2] = g_fast_linear_to_srgb.convert(c[2]);
o[3] = (uint8_t)fast_roundf_int(c[3]);
}
}
}
}
@@ -3692,5 +3910,69 @@ namespace basisu
return true;
}
bool tonemap_image_compressive2(image& dst_img, const imagef& hdr_test_img)
{
const uint32_t width = hdr_test_img.get_width();
const uint32_t height = hdr_test_img.get_height();
dst_img.resize(width, height);
dst_img.set_all(color_rgba(0, 0, 0, 255));
basisu::vector<basist::half_float> half_img(width * 3 * height);
uint32_t low_h = UINT32_MAX, high_h = 0;
for (uint32_t y = 0; y < height; y++)
{
for (uint32_t x = 0; x < width; x++)
{
const vec4F& p = hdr_test_img(x, y);
for (uint32_t i = 0; i < 3; i++)
{
float f = p[i];
if (std::isnan(f) || std::isinf(f))
f = 0.0f;
else if (f < 0.0f)
f = 0.0f;
else if (f > basist::MAX_HALF_FLOAT)
f = basist::MAX_HALF_FLOAT;
uint32_t h = basist::float_to_half(f);
low_h = minimum(low_h, h);
high_h = maximum(high_h, h);
half_img[(x + y * width) * 3 + i] = (basist::half_float)h;
} // i
} // x
} // y
if (low_h == high_h)
return false;
for (uint32_t y = 0; y < height; y++)
{
for (uint32_t x = 0; x < width; x++)
{
for (uint32_t i = 0; i < 3; i++)
{
basist::half_float h = half_img[(x + y * width) * 3 + i];
float f = (float)(h - low_h) / (float)(high_h - low_h);
int iv = basisu::clamp<int>((int)std::round(f * 255.0f), 0, 255);
dst_img(x, y)[i] = (uint8_t)iv;
} // i
} // x
} // y
return true;
}
} // namespace basisu