From d39d55f974a614332651fe357e90fcd855f66d2a Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Wed, 24 Mar 2021 11:38:18 +0100 Subject: [PATCH 01/19] Fix typos --- src/refresh/vkpt/bsp_mesh.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/refresh/vkpt/bsp_mesh.c b/src/refresh/vkpt/bsp_mesh.c index 7b495b59e..e1c7890d4 100644 --- a/src/refresh/vkpt/bsp_mesh.c +++ b/src/refresh/vkpt/bsp_mesh.c @@ -727,7 +727,7 @@ is_light_material(uint32_t material) } static void -collect_ligth_polys(bsp_mesh_t *wm, bsp_t *bsp, int model_idx, int* num_lights, int* allocated_lights, light_poly_t** lights) +collect_light_polys(bsp_mesh_t *wm, bsp_t *bsp, int model_idx, int* num_lights, int* allocated_lights, light_poly_t** lights) { mface_t *surfaces = model_idx < 0 ? bsp->faces : bsp->models[model_idx].firstface; int num_faces = model_idx < 0 ? bsp->numfaces : bsp->models[model_idx].numfaces; @@ -986,7 +986,7 @@ collect_ligth_polys(bsp_mesh_t *wm, bsp_t *bsp, int model_idx, int* num_lights, } static void -collect_sky_and_lava_ligth_polys(bsp_mesh_t *wm, bsp_t* bsp) +collect_sky_and_lava_light_polys(bsp_mesh_t *wm, bsp_t* bsp) { for (int i = 0; i < bsp->numfaces; i++) { @@ -1651,7 +1651,7 @@ bsp_mesh_create_from_bsp(bsp_mesh_t *wm, bsp_t *bsp, const char* map_name) wm->materials = Z_Malloc(MAX_VERT_BSP / 3 * sizeof(*wm->materials)); wm->clusters = Z_Malloc(MAX_VERT_BSP / 3 * sizeof(*wm->clusters)); - // clear these here because `bsp_mesh_load_custom_sky` creates lights before `collect_ligth_polys` + // clear these here because `bsp_mesh_load_custom_sky` creates lights before `collect_light_polys` wm->num_light_polys = 0; wm->allocated_light_polys = 0; wm->light_polys = NULL; @@ -1735,8 +1735,8 @@ bsp_mesh_create_from_bsp(bsp_mesh_t *wm, bsp_t *bsp, const char* map_name) compute_cluster_aabbs(wm); - collect_ligth_polys(wm, bsp, -1, &wm->num_light_polys, &wm->allocated_light_polys, &wm->light_polys); - collect_sky_and_lava_ligth_polys(wm, bsp); + collect_light_polys(wm, bsp, -1, &wm->num_light_polys, &wm->allocated_light_polys, &wm->light_polys); + collect_sky_and_lava_light_polys(wm, bsp); for (int k = 0; k < bsp->nummodels; k++) { @@ -1746,7 +1746,7 @@ bsp_mesh_create_from_bsp(bsp_mesh_t *wm, bsp_t *bsp, const char* map_name) model->allocated_light_polys = 0; model->light_polys = NULL; - collect_ligth_polys(wm, bsp, k, &model->num_light_polys, &model->allocated_light_polys, &model->light_polys); + collect_light_polys(wm, bsp, k, &model->num_light_polys, &model->allocated_light_polys, &model->light_polys); model->transparent = is_model_transparent(wm, model); } From 23e34117684028d82f5eb49752af376eaa9f0795 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Wed, 24 Mar 2021 14:10:25 +0100 Subject: [PATCH 02/19] Add MAT_IsCustom --- src/refresh/vkpt/material.c | 5 +++++ src/refresh/vkpt/material.h | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/refresh/vkpt/material.c b/src/refresh/vkpt/material.c index 4e6b62145..026594d15 100644 --- a/src/refresh/vkpt/material.c +++ b/src/refresh/vkpt/material.c @@ -664,3 +664,8 @@ qboolean MAT_IsKind(uint32_t material, uint32_t kind) { return (material & MATERIAL_KIND_MASK) == kind; } + +qboolean MAT_IsCustom(uint32_t material) +{ + return (material & MATERIAL_INDEX_MASK) >= pbr_materials_table.num_materials; +} diff --git a/src/refresh/vkpt/material.h b/src/refresh/vkpt/material.h index 3b7de8502..97edddd16 100644 --- a/src/refresh/vkpt/material.h +++ b/src/refresh/vkpt/material.h @@ -97,4 +97,7 @@ uint32_t MAT_SetKind(uint32_t material, uint32_t kind); // tests if the material is of a given kind qboolean MAT_IsKind(uint32_t material, uint32_t kind); +// tests if the material is "custom" (not in materials.csv) +qboolean MAT_IsCustom(uint32_t material); + #endif // __MATERIAL_H_ From 3c112824499c3a45ede3691b5670d5143303d079 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Wed, 24 Mar 2021 13:39:34 +0100 Subject: [PATCH 03/19] Synthesize material for surfaces with LIGHT flags --- src/refresh/vkpt/bsp_mesh.c | 40 ++++++++++++++++++++++++++ src/refresh/vkpt/main.c | 13 +++++++++ src/refresh/vkpt/material.c | 56 ++++++++++++++++++++++++++++++++----- src/refresh/vkpt/material.h | 3 ++ 4 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/refresh/vkpt/bsp_mesh.c b/src/refresh/vkpt/bsp_mesh.c index e1c7890d4..7060b94a4 100644 --- a/src/refresh/vkpt/bsp_mesh.c +++ b/src/refresh/vkpt/bsp_mesh.c @@ -28,6 +28,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include extern cvar_t *cvar_pt_enable_nodraw; +extern cvar_t *cvar_pt_enable_surface_lights; +extern cvar_t *cvar_pt_enable_surface_lights_warp; static void remove_collinear_edges(float* positions, float* tex_coords, int* num_vertices) @@ -1804,6 +1806,44 @@ bsp_mesh_register_textures(bsp_t *bsp) vkpt_normalize_normal_map(images.normals); } + if(cvar_pt_enable_surface_lights->value) + { + /* Synthesize an emissive material if the BSP surface has the LIGHT flag but the + material has no emissive image. + - Skip SKY and NODRAW surfaces, they'll be handled differently. + - Make WARP surfaces optional, as giving water, slime... an emissive texture clashes visually. */ + qboolean synth_surface_material = ((info->c.flags & (SURF_LIGHT | SURF_SKY | SURF_NODRAW)) == SURF_LIGHT) + && (info->radiance != 0); + qboolean needs_emissive = synth_surface_material && (images.emissive == NULL); + + qboolean is_warp_surface = (info->c.flags & SURF_WARP) != 0; + /* HACK: If set, assign an "emissive" texture to the material, + but then set material ID to the original (non-emissive) one. + This causes the surface to emit light, but no emissive component + appears when the surface is rendered */ + qboolean warp_surface_hack = is_warp_surface && (cvar_pt_enable_surface_lights_warp->value == 1); + + qboolean material_custom = MAT_IsCustom(mat->flags); + synth_surface_material &= (cvar_pt_enable_surface_lights->value >= 2) || material_custom || warp_surface_hack; + if(cvar_pt_enable_surface_lights_warp->value == 0) + synth_surface_material &= !is_warp_surface; + if(synth_surface_material && needs_emissive) + { + pbr_material_t *new_mat = MAT_CloneForRadiance(mat, info->radiance); + images.emissive = images.diffuse; + if (warp_surface_hack) + { + new_mat->flags = (new_mat->flags & ~MATERIAL_INDEX_MASK) | (mat->flags & MATERIAL_INDEX_MASK); + } + mat = new_mat; + } + else if(needs_emissive && !material_custom) + { + // Print something for materials listed in materials.csv + Com_DPrintf("Material '%s' used on LIGHT surface doesn't have emissive image\n", info->name); + } + } + if (images.emissive && !images.emissive->processing_complete && (mat->emissive_scale > 0.f) && ((mat->flags & MATERIAL_FLAG_LIGHT) != 0 || MAT_IsKind(mat->flags, MATERIAL_KIND_LAVA))) { vkpt_extract_emissive_texture_info(images.emissive); diff --git a/src/refresh/vkpt/main.c b/src/refresh/vkpt/main.c index 082f5c997..4626fbd31 100644 --- a/src/refresh/vkpt/main.c +++ b/src/refresh/vkpt/main.c @@ -51,6 +51,8 @@ cvar_t *cvar_profiler = NULL; cvar_t *cvar_vsync = NULL; cvar_t *cvar_pt_caustics = NULL; cvar_t *cvar_pt_enable_nodraw = NULL; +cvar_t *cvar_pt_enable_surface_lights = NULL; +cvar_t *cvar_pt_enable_surface_lights_warp = NULL; cvar_t *cvar_pt_accumulation_rendering = NULL; cvar_t *cvar_pt_accumulation_rendering_framenum = NULL; cvar_t *cvar_pt_projection = NULL; @@ -3404,6 +3406,17 @@ R_Init_RTX(qboolean total) cvar_vsync->changed = NULL; // in case the GL renderer has set it cvar_pt_caustics = Cvar_Get("pt_caustics", "1", CVAR_ARCHIVE); cvar_pt_enable_nodraw = Cvar_Get("pt_enable_nodraw", "0", 0); + /* Synthesize materials for surfaces with LIGHT flag. + * 0: disabled + * 1: enabled for "custom" materials (not in materials.csv) + * 2: enabled for all materials w/o an emissive texture */ + cvar_pt_enable_surface_lights = Cvar_Get("pt_enable_surface_lights", "1", CVAR_FILES); + /* LIGHT flag synthesis for "warp" surfaces (water, slime), + * separately controlled for aesthetic reasons + * 0: disabled + * 1: hack up a material that emits light but doesn't render with an emissive texture + * 2: "full" synthesis (incl emissive texture) */ + cvar_pt_enable_surface_lights_warp = Cvar_Get("pt_enable_surface_lights_warp", "0", CVAR_FILES); // 0 -> disabled, regular pause; 1 -> enabled; 2 -> enabled, hide GUI cvar_pt_accumulation_rendering = Cvar_Get("pt_accumulation_rendering", "1", CVAR_ARCHIVE); diff --git a/src/refresh/vkpt/material.c b/src/refresh/vkpt/material.c index 026594d15..3e9ecef59 100644 --- a/src/refresh/vkpt/material.c +++ b/src/refresh/vkpt/material.c @@ -17,6 +17,7 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "material.h" +#include "common/common.h" #include "common/files.h" #include "refresh/images.h" #include "vk_util.h" @@ -134,6 +135,8 @@ typedef struct pbr_materials_table_s { static pbr_materials_table_t pbr_materials_table = { .num_materials = 0, .num_custom_materials = 0, .alpha_sorted = qtrue }; +static pbr_material_t *find_existing_pbr_material(char const *name); + pbr_material_t const * MAT_GetPBRMaterialsTable() { return &pbr_materials_table.materials[0]; @@ -239,6 +242,10 @@ static qerror_t writeMaterialsTable(char const * filename, pbr_materials_table_t { pbr_material_t const * mat = &table->materials[i]; + // Skip materials generated for LIGHT surfaces + if(strchr(mat->name, '*') != NULL) + continue; + FS_FPrintf(f, "%s,", mat->name); FS_FPrintf(f, "%g,%g,%g,%g,", mat->bump_scale, mat->rough_override, mat->specular_scale, mat->emissive_scale); @@ -391,6 +398,31 @@ qerror_t MAT_SavePBRMaterials() return writeMaterialsTable(materials_filename, &pbr_materials_table); } +pbr_material_t *MAT_CloneForRadiance(pbr_material_t *src_mat, int radiance) +{ + char clone_name[MAX_QPATH]; + Q_snprintf(clone_name, sizeof(clone_name), "%s*%d", src_mat->name, radiance); + pbr_material_t *mat = find_existing_pbr_material(clone_name); + + if(mat) + return mat; + + // not found - create a material entry + Com_DPrintf("Synthesizing material for 'emissive' surface with '%s' (%d)\n", src_mat->name, radiance); + pbr_materials_table_t * table = &pbr_materials_table; + int index = table->num_materials + table->num_custom_materials; + table->num_custom_materials++; + mat = &table->materials[index]; + memcpy(mat, src_mat, sizeof(pbr_material_t)); + strcpy(mat->name, clone_name); + mat->next_frame = index; + mat->flags &= ~MATERIAL_INDEX_MASK; + mat->flags |= index; + mat->flags |= MATERIAL_FLAG_LIGHT; + mat->emissive_scale = radiance * 0.001f; + + return mat; +} qerror_t MAT_RegisterPBRMaterial(pbr_material_t * mat, image_t * image_diffuse, image_t * image_normals, image_t * image_emissive) { @@ -460,12 +492,8 @@ pbr_material_t * MAT_GetPBRMaterial(int index) } -pbr_material_t * MAT_FindPBRMaterial(char const * name) +static pbr_material_t * find_existing_pbr_material(char const * name) { - char name_copy[MAX_QPATH]; - int len = truncateExtension(name, name_copy); - assert(len>0); - pbr_materials_table_t * table = &pbr_materials_table; // note : key comparison must be case insensitive @@ -481,7 +509,7 @@ pbr_material_t * MAT_FindPBRMaterial(char const * name) { int middle = floor((left + right) / 2); pbr_material_t * mat = &table->materials[middle]; - int cmp = Q_strcasecmp(name_copy, mat->name); + int cmp = Q_strcasecmp(name, mat->name); if (cmp < 0) right = middle - 1; else if (cmp > 0) @@ -497,11 +525,25 @@ pbr_material_t * MAT_FindPBRMaterial(char const * name) for (int i = search_start; i < table->num_materials + table->num_custom_materials; ++i) { pbr_material_t * mat = &table->materials[i]; - if (Q_strcasecmp(mat->name, name_copy) == 0) + if (Q_strcasecmp(mat->name, name) == 0) return mat; } + return NULL; +} + +pbr_material_t * MAT_FindPBRMaterial(char const * name) +{ + char name_copy[MAX_QPATH]; + int len = truncateExtension(name, name_copy); + assert(len>0); + + pbr_material_t *existing_mat = find_existing_pbr_material(name_copy); + if(existing_mat) + return existing_mat; + // not found - create a material entry + pbr_materials_table_t * table = &pbr_materials_table; int index = table->num_materials + table->num_custom_materials; table->num_custom_materials++; pbr_material_t * mat = &table->materials[index]; diff --git a/src/refresh/vkpt/material.h b/src/refresh/vkpt/material.h index 97edddd16..f0eb44129 100644 --- a/src/refresh/vkpt/material.h +++ b/src/refresh/vkpt/material.h @@ -49,6 +49,9 @@ typedef struct pbr_material_s { // returns index of given material in table int MAT_GetPBRMaterialIndex(pbr_material_t const * mat); +// Clone a material for use on a surface with LIGHT flag +pbr_material_t *MAT_CloneForRadiance(pbr_material_t *mat, int radiance); + // registration sequence : set material PBR textures qerror_t MAT_RegisterPBRMaterial(pbr_material_t * mat, image_t * image_diffuse, image_t * image_normals, image_t * image_emissive); From 7ec4005fa507bdfc867ae24fef0620650b1bd714 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Thu, 25 Mar 2021 12:50:16 +0100 Subject: [PATCH 04/19] Add Vector4MA --- inc/shared/shared.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 251dff42d..fea1cad7d 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -262,6 +262,11 @@ static inline float Q_fabs(float f) #define Vector4Clear(a) ((a)[0]=(a)[1]=(a)[2]=(a)[3]=0) #define Vector4Negate(a,b) ((b)[0]=-(a)[0],(b)[1]=-(a)[1],(b)[2]=-(a)[2],(b)[3]=-(a)[3]) #define Vector4Set(v, a, b, c, d) ((v)[0]=(a),(v)[1]=(b),(v)[2]=(c),(v)[3]=(d)) +#define Vector4MA(a,b,c,d) \ + ((d)[0]=(a)[0]+(b)*(c)[0], \ + (d)[1]=(a)[1]+(b)*(c)[1], \ + (d)[2]=(a)[2]+(b)*(c)[2], \ + (d)[3]=(a)[3]+(b)*(c)[3]) #define QuatCopy(a,b) ((b)[0]=(a)[0],(b)[1]=(a)[1],(b)[2]=(a)[2],(b)[3]=(a)[3]) From 45d3577f10e0e55836a82273e89369184c08e6bd Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Thu, 25 Mar 2021 18:17:17 +0100 Subject: [PATCH 05/19] Add IMG_Clone() --- inc/refresh/images.h | 1 + src/refresh/images.c | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/inc/refresh/images.h b/inc/refresh/images.h index cb8dda8b9..d721edc4f 100644 --- a/inc/refresh/images.h +++ b/inc/refresh/images.h @@ -107,6 +107,7 @@ extern uint32_t d_8to24table[256]; // these are implemented in src/refresh/images.c void IMG_ReloadAll(); image_t *IMG_Find(const char *name, imagetype_t type, imageflags_t flags); +image_t *IMG_Clone(image_t *image); void IMG_FreeUnused(void); void IMG_FreeAll(void); void IMG_Init(void); diff --git a/src/refresh/images.c b/src/refresh/images.c index a9034fd0c..76d53588d 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1188,6 +1188,47 @@ image_t *IMG_Find(const char *name, imagetype_t type, imageflags_t flags) return R_NOTEXTURE; } +/* +=============== +IMG_Clone +=============== +*/ +image_t *IMG_Clone(image_t *image) +{ + if(image == R_NOTEXTURE) + return image; + + image_t* new_image = alloc_image(); + if (!new_image) + return R_NOTEXTURE; + + memcpy(new_image, image, sizeof(image_t)); + +#if USE_REF == REF_VKPT + size_t image_size = image->upload_width * image->upload_height * 4; + if(image->pix_data != NULL) + { + new_image->pix_data = IMG_AllocPixels(image_size); + memcpy(new_image->pix_data, image->pix_data, image_size); + } +#else + for (int m = 0; m < 4; m++) + { + if(image->pixels[m] != NULL) + { + size_t mip_size = (image->upload_width >> m) * (image->upload_height >> m) * 4; + new_image->pixels[m] = IMG_AllocPixels(mip_size); + memcpy(new_image->pixels[m], image->pixels[m], mip_size); + } + } +#endif + + + unsigned hash = FS_HashPathLen(new_image->name, new_image->baselen, RIMAGES_HASH); + List_Append(&r_imageHash[hash], &new_image->entry); + return new_image; +} + /* =============== IMG_ForHandle From 9addd9d8639eec2f28db156f445510e0f4103d2f Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Thu, 25 Mar 2021 18:17:43 +0100 Subject: [PATCH 06/19] Add a more elaborate method to guess an emissive texture from diffuse --- inc/refresh/refresh.h | 1 + src/refresh/vkpt/bsp_mesh.c | 27 ++++- src/refresh/vkpt/main.c | 5 + src/refresh/vkpt/textures.c | 202 ++++++++++++++++++++++++++++++++++++ src/refresh/vkpt/vkpt.h | 1 + 5 files changed, 231 insertions(+), 5 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 28f7e5dca..394069582 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -202,6 +202,7 @@ typedef enum { IF_NEAREST = (1 << 7), IF_OPAQUE = (1 << 8), IF_SRGB = (1 << 9), + IF_FAKE_EMISSIVE= (1 << 10), // Image source indicator/requirement flags IF_SRC_BASE = (0x1 << 16), diff --git a/src/refresh/vkpt/bsp_mesh.c b/src/refresh/vkpt/bsp_mesh.c index 7060b94a4..22414da30 100644 --- a/src/refresh/vkpt/bsp_mesh.c +++ b/src/refresh/vkpt/bsp_mesh.c @@ -30,6 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., extern cvar_t *cvar_pt_enable_nodraw; extern cvar_t *cvar_pt_enable_surface_lights; extern cvar_t *cvar_pt_enable_surface_lights_warp; +extern cvar_t *cvar_pt_surface_lights_fake_emissive_algo; static void remove_collinear_edges(float* positions, float* tex_coords, int* num_vertices) @@ -1779,6 +1780,19 @@ bsp_mesh_destroy(bsp_mesh_t *wm) memset(wm, 0, sizeof(*wm)); } +static image_t* get_fake_emissive_image(image_t* diffuse) +{ + switch(cvar_pt_surface_lights_fake_emissive_algo->integer) + { + case 0: + return diffuse; + case 1: + return vkpt_fake_emissive_texture(diffuse); + default: + return NULL; + } +} + void bsp_mesh_register_textures(bsp_t *bsp) { @@ -1829,13 +1843,16 @@ bsp_mesh_register_textures(bsp_t *bsp) synth_surface_material &= !is_warp_surface; if(synth_surface_material && needs_emissive) { - pbr_material_t *new_mat = MAT_CloneForRadiance(mat, info->radiance); - images.emissive = images.diffuse; - if (warp_surface_hack) + images.emissive = get_fake_emissive_image(images.diffuse); + if(images.emissive) { - new_mat->flags = (new_mat->flags & ~MATERIAL_INDEX_MASK) | (mat->flags & MATERIAL_INDEX_MASK); + pbr_material_t *new_mat = MAT_CloneForRadiance(mat, info->radiance); + if (warp_surface_hack) + { + new_mat->flags = (new_mat->flags & ~MATERIAL_INDEX_MASK) | (mat->flags & MATERIAL_INDEX_MASK); + } + mat = new_mat; } - mat = new_mat; } else if(needs_emissive && !material_custom) { diff --git a/src/refresh/vkpt/main.c b/src/refresh/vkpt/main.c index 4626fbd31..149f55ff1 100644 --- a/src/refresh/vkpt/main.c +++ b/src/refresh/vkpt/main.c @@ -53,6 +53,7 @@ cvar_t *cvar_pt_caustics = NULL; cvar_t *cvar_pt_enable_nodraw = NULL; cvar_t *cvar_pt_enable_surface_lights = NULL; cvar_t *cvar_pt_enable_surface_lights_warp = NULL; +cvar_t *cvar_pt_surface_lights_fake_emissive_algo = NULL; cvar_t *cvar_pt_accumulation_rendering = NULL; cvar_t *cvar_pt_accumulation_rendering_framenum = NULL; cvar_t *cvar_pt_projection = NULL; @@ -3417,6 +3418,10 @@ R_Init_RTX(qboolean total) * 1: hack up a material that emits light but doesn't render with an emissive texture * 2: "full" synthesis (incl emissive texture) */ cvar_pt_enable_surface_lights_warp = Cvar_Get("pt_enable_surface_lights_warp", "0", CVAR_FILES); + /* How to choose emissive texture for LIGHT flag synthesis: + * 0: Just use diffuse texture + * 1: Use (diffuse) pixels above a certain relative brightness for emissive texture */ + cvar_pt_surface_lights_fake_emissive_algo = Cvar_Get("pt_surface_lights_fake_emissive_algo", "1", CVAR_FILES); // 0 -> disabled, regular pause; 1 -> enabled; 2 -> enabled, hide GUI cvar_pt_accumulation_rendering = Cvar_Get("pt_accumulation_rendering", "1", CVAR_ARCHIVE); diff --git a/src/refresh/vkpt/textures.c b/src/refresh/vkpt/textures.c index 57e0a6fd7..af26a39b3 100644 --- a/src/refresh/vkpt/textures.c +++ b/src/refresh/vkpt/textures.c @@ -522,6 +522,204 @@ static inline byte encode_linear(float x) return (byte)roundf(x * 255.f); } +static inline void decode_srgba(float* dest, const byte* src) +{ + dest[0] = decode_srgb(src[0]); + dest[1] = decode_srgb(src[1]); + dest[2] = decode_srgb(src[2]); + dest[3] = decode_linear(src[3]); +} + +static inline void encode_srgba(byte* dest, const float* src) +{ + dest[0] = encode_srgb(src[0]); + dest[1] = encode_srgb(src[1]); + dest[2] = encode_srgb(src[2]); + dest[3] = encode_linear(src[3]); +} + +struct filterscratch_s +{ + int pad_left, pad_right; + vec4_t *ptr; +}; + +static void filterscratch_init(struct filterscratch_s* scratch, unsigned kernel_size, int stripe_size) +{ + scratch->pad_left = kernel_size / 2; + scratch->pad_right = kernel_size - scratch->pad_left - 1; + int num_scratch_pixels = scratch->pad_left + stripe_size + scratch->pad_right; + scratch->ptr = Z_Malloc(num_scratch_pixels * sizeof(vec4_t)); +} + +static void filterscratch_free(struct filterscratch_s* scratch) +{ + Z_Free(scratch->ptr); +} + +static void filterscratch_fill_from_image(struct filterscratch_s *scratch, byte *current_stripe, int stripe_size, int element_stride) +{ + int src = -scratch->pad_left; + vec4_t *dest_ptr = scratch->ptr; + if ((stripe_size >= scratch->pad_left) && (stripe_size >= scratch->pad_right)) + { + for (; src < 0; src++) + { + const byte *src_data = current_stripe + (src + stripe_size) * element_stride * 4; + decode_srgba((float*)(dest_ptr++), src_data); + } + for (; src < stripe_size; src++) + { + const byte *src_data = current_stripe + src * element_stride * 4; + decode_srgba((float*)(dest_ptr++), src_data); + } + for (; src < stripe_size + scratch->pad_right; src++) + { + const byte *src_data = current_stripe + (src - stripe_size) * element_stride * 4; + decode_srgba((float*)(dest_ptr++), src_data); + } + } + else + { + // The (probably) rarer case - filter kernel is larger than image + while(src < 0) + src += stripe_size; + for (int i = 0; i < scratch->pad_left + stripe_size + scratch->pad_right; i++) + { + const byte *src_data = current_stripe + src * element_stride * 4; + decode_srgba((float*)(dest_ptr++), src_data); + src = (src + 1) % stripe_size; + } + } +} + +/* Apply a (separable) filter along one dimension of an image. + * Whether this is done along the X or Y dimension depends on the "stripe size" + * and "stripe stride" options. See filter_image() for how to use it practically. */ +static void filter_one_dimension(byte* pixels, const float kernel[], unsigned kernel_size, + int stripe_size, int num_stripes, int stripe_stride, int element_stride) +{ + struct filterscratch_s scratch; + filterscratch_init(&scratch, kernel_size, stripe_size); + byte *current_stripe = pixels; + for (int s = 0; s < num_stripes; s++) + { + // back up image data to scratch buffer + filterscratch_fill_from_image(&scratch, current_stripe, stripe_size, element_stride); + // filter the stripe + for (int i = 0; i < stripe_size; i++) + { + vec4_t color; + memset(color, 0, sizeof(color)); + for (int j = 0; j < kernel_size; j++) + { + float f = kernel[j]; + vec4_t *src_p = scratch.ptr + i + j; + Vector4MA(color, f, *src_p, color); + } + encode_srgba(current_stripe + i * element_stride * 4, color); + } + current_stripe += stripe_stride * 4; + } + filterscratch_free(&scratch); +} + +// Apply a (separable) filter to an image. +static void filter_image(byte* pixels, const float kernel[], unsigned kernel_size, int width, int height) +{ + // Filter horizontally + filter_one_dimension(pixels, kernel, kernel_size, width, height, width, 1); + // Filter vertically + filter_one_dimension(pixels, kernel, kernel_size, height, width, 1, width); +} + +// Fake an emissive texture from a diffuse texture by using pixels brighter than a certain amount +static void apply_fake_emissive_threshold(image_t *image) +{ + int w = image->upload_width; + int h = image->upload_height; + + byte* current_pixel = image->pix_data; + + float max_lum = 0; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + vec3_t color; + color[0] = decode_srgb(current_pixel[0]); + color[1] = decode_srgb(current_pixel[1]); + color[2] = decode_srgb(current_pixel[2]); + float lum = LUMINANCE(color[0], color[1], color[2]); + if (lum > max_lum) + max_lum = lum; + + current_pixel += 4; + } + } + + float threshold = max_lum * 0.68f; // FIXME: Empirical guess. Perhaps decide using some more clever algorithm? + + current_pixel = image->pix_data; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + vec3_t color; + color[0] = decode_srgb(current_pixel[0]); + color[1] = decode_srgb(current_pixel[1]); + color[2] = decode_srgb(current_pixel[2]); + float lum = LUMINANCE(color[0], color[1], color[2]); + if(lum < threshold) + { + current_pixel[0] = 0; + current_pixel[1] = 0; + current_pixel[2] = 0; + } + else + { + float new_lum = (lum - threshold) / (max_lum - threshold); + VectorScale(color, new_lum / lum, color); + current_pixel[0] = encode_srgb(color[0]); + current_pixel[1] = encode_srgb(color[1]); + current_pixel[2] = encode_srgb(color[2]); + } + + current_pixel += 4; + } + } + + // Scale up - this makes low-res textures look bit better, together with the blur below + int new_width = w * 2; + int new_height = h * 2; + int new_size = new_width * new_height * 4; + byte *new_data = IMG_AllocPixels(new_size); + IMG_ResampleTexture(image->pix_data, w, h, new_data, new_width, new_height); + byte *old_data = image->pix_data; + image->pix_data = new_data; + Z_Free(old_data); + w = image->upload_width = new_width; + h = image->upload_height = new_height; + + // Apply a blur + const float filter[] = { 0.00598f, 0.060626f, 0.241843f, 0.383103f, 0.241843f, 0.060626f, 0.00598f }; + filter_image(image->pix_data, filter, sizeof(filter) / sizeof(filter[0]), w, h); +} + +image_t *vkpt_fake_emissive_texture(image_t *image) +{ + if((image->upload_width == 1) && (image->upload_height == 1)) + { + // Not much to do... + return image; + } + + image_t *new_image = IMG_Clone(image); + if(new_image == R_NOTEXTURE) + return image; + + new_image->flags |= IF_FAKE_EMISSIVE; + apply_fake_emissive_threshold(new_image); + + return new_image; +} + void vkpt_extract_emissive_texture_info(image_t *image) { @@ -730,6 +928,10 @@ void IMG_ReloadAll(void) { vkpt_normalize_normal_map(image); } + if(image->flags & IF_FAKE_EMISSIVE) + { + apply_fake_emissive_threshold(image); + } image->last_modified = last_modifed; // reset time stamp because load_img doesn't diff --git a/src/refresh/vkpt/vkpt.h b/src/refresh/vkpt/vkpt.h index c98a9d74a..b3b82eed3 100644 --- a/src/refresh/vkpt/vkpt.h +++ b/src/refresh/vkpt/vkpt.h @@ -527,6 +527,7 @@ VkResult vkpt_textures_upload_envmap(int w, int h, byte *data); void vkpt_textures_destroy_unused(); void vkpt_textures_update_descriptor_set(); void vkpt_normalize_normal_map(image_t *image); +image_t *vkpt_fake_emissive_texture(image_t *image); void vkpt_extract_emissive_texture_info(image_t *image); void vkpt_textures_prefetch(); void vkpt_invalidate_texture_descriptors(); From 70a50694b0a953e396a2258bfd23956ac1b3a60f Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Thu, 15 Apr 2021 19:39:22 +0200 Subject: [PATCH 07/19] Also set custom material count to 0 in parseMaterialsTable() Required for proper reloads with custom materials present. --- src/refresh/vkpt/material.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/refresh/vkpt/material.c b/src/refresh/vkpt/material.c index 3e9ecef59..e6344a3e6 100644 --- a/src/refresh/vkpt/material.c +++ b/src/refresh/vkpt/material.c @@ -268,6 +268,7 @@ static qerror_t parseMaterialsTable(char const * filename, pbr_materials_table_t assert(table); table->num_materials = 0; + table->num_custom_materials = 0; byte * buffer = NULL; ssize_t buffer_size = 0; buffer_size = FS_LoadFile(filename, (void**)&buffer); From 67eea9015b3e0a94730c3cd598d76086594944d1 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Thu, 15 Apr 2021 20:36:58 +0200 Subject: [PATCH 08/19] Only create 'fake emissive' image once --- src/refresh/vkpt/bsp_mesh.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/refresh/vkpt/bsp_mesh.c b/src/refresh/vkpt/bsp_mesh.c index 22414da30..055d0c585 100644 --- a/src/refresh/vkpt/bsp_mesh.c +++ b/src/refresh/vkpt/bsp_mesh.c @@ -1843,15 +1843,20 @@ bsp_mesh_register_textures(bsp_t *bsp) synth_surface_material &= !is_warp_surface; if(synth_surface_material && needs_emissive) { - images.emissive = get_fake_emissive_image(images.diffuse); - if(images.emissive) + pbr_material_t *new_mat = MAT_CloneForRadiance(mat, info->radiance); + if (warp_surface_hack) { - pbr_material_t *new_mat = MAT_CloneForRadiance(mat, info->radiance); - if (warp_surface_hack) - { - new_mat->flags = (new_mat->flags & ~MATERIAL_INDEX_MASK) | (mat->flags & MATERIAL_INDEX_MASK); - } - mat = new_mat; + new_mat->flags = (new_mat->flags & ~MATERIAL_INDEX_MASK) | (mat->flags & MATERIAL_INDEX_MASK); + } + mat = new_mat; + if(mat->image_emissive && (mat->image_emissive != R_NOTEXTURE)) + { + // Use previously created emissive image + images.emissive = mat->image_emissive; + } + else + { + images.emissive = get_fake_emissive_image(images.diffuse); } } else if(needs_emissive && !material_custom) From eaed4ac245edda79021315a2e7dfa34b29ca54b6 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Thu, 15 Apr 2021 20:54:42 +0200 Subject: [PATCH 09/19] Reset registration_sequence when cloning a material --- src/refresh/vkpt/material.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/refresh/vkpt/material.c b/src/refresh/vkpt/material.c index e6344a3e6..aa755d21f 100644 --- a/src/refresh/vkpt/material.c +++ b/src/refresh/vkpt/material.c @@ -420,6 +420,7 @@ pbr_material_t *MAT_CloneForRadiance(pbr_material_t *src_mat, int radiance) mat->flags &= ~MATERIAL_INDEX_MASK; mat->flags |= index; mat->flags |= MATERIAL_FLAG_LIGHT; + mat->registration_sequence = 0; mat->emissive_scale = radiance * 0.001f; return mat; From 2a25f36e45a02aa48332f0999f83b4e64b3623e8 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Sat, 29 May 2021 19:46:38 +0200 Subject: [PATCH 10/19] Add 'new name' parameter to IMG_Clone() --- inc/refresh/images.h | 2 +- src/refresh/images.c | 11 +++++++++-- src/refresh/vkpt/textures.c | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/inc/refresh/images.h b/inc/refresh/images.h index d721edc4f..cebffb801 100644 --- a/inc/refresh/images.h +++ b/inc/refresh/images.h @@ -107,7 +107,7 @@ extern uint32_t d_8to24table[256]; // these are implemented in src/refresh/images.c void IMG_ReloadAll(); image_t *IMG_Find(const char *name, imagetype_t type, imageflags_t flags); -image_t *IMG_Clone(image_t *image); +image_t *IMG_Clone(image_t *image, const char* new_name); void IMG_FreeUnused(void); void IMG_FreeAll(void); void IMG_Init(void); diff --git a/src/refresh/images.c b/src/refresh/images.c index 76d53588d..e9775a143 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -32,6 +32,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "stb_image.h" #include "stb_image_write.h" +#include + #define R_COLORMAP_PCX "pics/colormap.pcx" #define IMG_LOAD(x) \ @@ -1193,7 +1195,7 @@ image_t *IMG_Find(const char *name, imagetype_t type, imageflags_t flags) IMG_Clone =============== */ -image_t *IMG_Clone(image_t *image) +image_t *IMG_Clone(image_t *image, const char* new_name) { if(image == R_NOTEXTURE) return image; @@ -1223,7 +1225,12 @@ image_t *IMG_Clone(image_t *image) } #endif - + if(new_name) + { + Q_strlcpy(new_image->name, new_name, sizeof(new_image->name)); + new_image->baselen = strlen(new_image->name) - 4; + assert(new_image->name[new_image->baselen] == '.'); + } unsigned hash = FS_HashPathLen(new_image->name, new_image->baselen, RIMAGES_HASH); List_Append(&r_imageHash[hash], &new_image->entry); return new_image; diff --git a/src/refresh/vkpt/textures.c b/src/refresh/vkpt/textures.c index af26a39b3..71ad878b8 100644 --- a/src/refresh/vkpt/textures.c +++ b/src/refresh/vkpt/textures.c @@ -710,7 +710,7 @@ image_t *vkpt_fake_emissive_texture(image_t *image) return image; } - image_t *new_image = IMG_Clone(image); + image_t *new_image = IMG_Clone(image, NULL); if(new_image == R_NOTEXTURE) return image; From 7e8208aa07d2694541f92af4e5f212443b2cf668 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Sat, 29 May 2021 19:48:32 +0200 Subject: [PATCH 11/19] Add IMG_FindExisting() --- inc/refresh/images.h | 1 + src/refresh/images.c | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/inc/refresh/images.h b/inc/refresh/images.h index cebffb801..b57b14ac2 100644 --- a/inc/refresh/images.h +++ b/inc/refresh/images.h @@ -107,6 +107,7 @@ extern uint32_t d_8to24table[256]; // these are implemented in src/refresh/images.c void IMG_ReloadAll(); image_t *IMG_Find(const char *name, imagetype_t type, imageflags_t flags); +image_t *IMG_FindExisting(const char *name, imagetype_t type); image_t *IMG_Clone(image_t *image, const char* new_name); void IMG_FreeUnused(void); void IMG_FreeAll(void); diff --git a/src/refresh/images.c b/src/refresh/images.c index e9775a143..4172409ad 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1190,6 +1190,40 @@ image_t *IMG_Find(const char *name, imagetype_t type, imageflags_t flags) return R_NOTEXTURE; } +image_t *IMG_FindExisting(const char *name, imagetype_t type) +{ + image_t *image; + size_t len; + unsigned hash; + + if (!name) { + Com_Error(ERR_FATAL, "%s: NULL", __func__); + } + + // this should never happen + len = strlen(name); + if (len >= MAX_QPATH) { + Com_Error(ERR_FATAL, "%s: oversize name", __func__); + } + + // must have an extension and at least 1 char of base name + if (len <= 4) { + return R_NOTEXTURE; + } + if (name[len - 4] != '.') { + return R_NOTEXTURE; + } + + hash = FS_HashPathLen(name, len - 4, RIMAGES_HASH); + + // look for it + if ((image = lookup_image(name, type, hash, len - 4)) != NULL) { + return image; + } + + return R_NOTEXTURE; +} + /* =============== IMG_Clone From cbb52dcbc34f36807f9b0e1384e5370f669423a1 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Sat, 29 May 2021 19:49:08 +0200 Subject: [PATCH 12/19] Reuse 'fake emissive' images previously created for the same diffuse image --- src/refresh/vkpt/textures.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/refresh/vkpt/textures.c b/src/refresh/vkpt/textures.c index 71ad878b8..906fe0e2e 100644 --- a/src/refresh/vkpt/textures.c +++ b/src/refresh/vkpt/textures.c @@ -710,7 +710,21 @@ image_t *vkpt_fake_emissive_texture(image_t *image) return image; } - image_t *new_image = IMG_Clone(image, NULL); + // Construct a new name for the fake emissive image + const char emissive_image_suffix[] = "*E.wal"; // 'fake' extension needed for image lookup logic + char emissive_image_name[MAX_QPATH]; + Q_strlcpy(emissive_image_name, image->name, sizeof(emissive_image_name)); + size_t pos = strlen(emissive_image_name) - 4; + if (pos + sizeof(emissive_image_suffix) > sizeof(emissive_image_name)) + pos = sizeof(emissive_image_name) - sizeof(emissive_image_suffix); + Q_strlcpy(emissive_image_name + pos, emissive_image_suffix, sizeof(emissive_image_name) - pos); + + // See if we previously created a fake emissive texture for the same base texture + image_t *prev_image = IMG_FindExisting(emissive_image_name, image->type); + if(prev_image != R_NOTEXTURE) + return prev_image; + + image_t *new_image = IMG_Clone(image, emissive_image_name); if(new_image == R_NOTEXTURE) return image; From 52142ec2837fe3e47d65011d9d92f71cdd78c39e Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Sun, 30 May 2021 22:00:34 +0200 Subject: [PATCH 13/19] Update registration_sequence when reusing 'fake emissive' image --- src/refresh/vkpt/textures.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/refresh/vkpt/textures.c b/src/refresh/vkpt/textures.c index 906fe0e2e..460b180cc 100644 --- a/src/refresh/vkpt/textures.c +++ b/src/refresh/vkpt/textures.c @@ -722,7 +722,10 @@ image_t *vkpt_fake_emissive_texture(image_t *image) // See if we previously created a fake emissive texture for the same base texture image_t *prev_image = IMG_FindExisting(emissive_image_name, image->type); if(prev_image != R_NOTEXTURE) + { + prev_image->registration_sequence = registration_sequence; return prev_image; + } image_t *new_image = IMG_Clone(image, emissive_image_name); if(new_image == R_NOTEXTURE) From 6345aa9914f3b7853e253d5955aa3973a50c8491 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Mon, 31 May 2021 23:35:07 +0200 Subject: [PATCH 14/19] Call MAT_UpdateRegistration() from MAT_RegisterPBRMaterial() We're using "just" MAT_RegisterPBRMaterial() for synthesized materials, even when reusing a previous material - but that case needs an image registration sequence update. So just "update" a material upon registration. --- src/refresh/vkpt/material.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/vkpt/material.c b/src/refresh/vkpt/material.c index aa755d21f..6f4083d69 100644 --- a/src/refresh/vkpt/material.c +++ b/src/refresh/vkpt/material.c @@ -438,12 +438,12 @@ qerror_t MAT_RegisterPBRMaterial(pbr_material_t * mat, image_t * image_diffuse, if (mat->registration_sequence == registration_sequence) return Q_ERR_SUCCESS; - mat->registration_sequence = registration_sequence; - mat->image_diffuse = image_diffuse; mat->image_normals = image_normals; mat->image_emissive = image_emissive; + MAT_UpdateRegistration(mat); + //if (mat->image_diffuse == R_NOTEXTURE) // mat->flags &= ~(MATERIAL_FLAG_VALID); From 910badd55e44a41624cae1e060dd322f841bdf54 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Thu, 27 May 2021 16:43:40 +0200 Subject: [PATCH 15/19] Change fake emissive generation Previously, the algorithn was to simply extract pixels above a certain dynamic brightness. Some scaling and blurring was applied as well. The results were usable but could be crude-looking. The new approach determines all 'bright' pixels (largest component above a certain) and applies some blurring as well as some filtering with the original image. After some experimentation with the details this resulted in an algorithm that seems to extract emissive images that seem to be relatively faithful to the appearance of the "light" part of original textures. --- src/refresh/vkpt/textures.c | 188 +++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 90 deletions(-) diff --git a/src/refresh/vkpt/textures.c b/src/refresh/vkpt/textures.c index 460b180cc..b9844b930 100644 --- a/src/refresh/vkpt/textures.c +++ b/src/refresh/vkpt/textures.c @@ -522,34 +522,20 @@ static inline byte encode_linear(float x) return (byte)roundf(x * 255.f); } -static inline void decode_srgba(float* dest, const byte* src) -{ - dest[0] = decode_srgb(src[0]); - dest[1] = decode_srgb(src[1]); - dest[2] = decode_srgb(src[2]); - dest[3] = decode_linear(src[3]); -} - -static inline void encode_srgba(byte* dest, const float* src) -{ - dest[0] = encode_srgb(src[0]); - dest[1] = encode_srgb(src[1]); - dest[2] = encode_srgb(src[2]); - dest[3] = encode_linear(src[3]); -} - struct filterscratch_s { + int num_comps; int pad_left, pad_right; - vec4_t *ptr; + float *ptr; }; -static void filterscratch_init(struct filterscratch_s* scratch, unsigned kernel_size, int stripe_size) +static void filterscratch_init(struct filterscratch_s* scratch, unsigned kernel_size, int stripe_size, int num_comps) { + scratch->num_comps = num_comps; scratch->pad_left = kernel_size / 2; scratch->pad_right = kernel_size - scratch->pad_left - 1; int num_scratch_pixels = scratch->pad_left + stripe_size + scratch->pad_right; - scratch->ptr = Z_Malloc(num_scratch_pixels * sizeof(vec4_t)); + scratch->ptr = Z_Malloc(num_scratch_pixels * num_comps * sizeof(float)); } static void filterscratch_free(struct filterscratch_s* scratch) @@ -557,26 +543,30 @@ static void filterscratch_free(struct filterscratch_s* scratch) Z_Free(scratch->ptr); } -static void filterscratch_fill_from_image(struct filterscratch_s *scratch, byte *current_stripe, int stripe_size, int element_stride) +static void filterscratch_fill_from_float_image(struct filterscratch_s *scratch, float *current_stripe, int stripe_size, int element_stride) { + const int num_comps = scratch->num_comps; int src = -scratch->pad_left; - vec4_t *dest_ptr = scratch->ptr; + float *dest_ptr = scratch->ptr; if ((stripe_size >= scratch->pad_left) && (stripe_size >= scratch->pad_right)) { for (; src < 0; src++) { - const byte *src_data = current_stripe + (src + stripe_size) * element_stride * 4; - decode_srgba((float*)(dest_ptr++), src_data); + const float *src_data = current_stripe + (src + stripe_size) * element_stride * num_comps; + memcpy(dest_ptr, src_data, num_comps * sizeof(float)); + dest_ptr += num_comps; } for (; src < stripe_size; src++) { - const byte *src_data = current_stripe + src * element_stride * 4; - decode_srgba((float*)(dest_ptr++), src_data); + const float *src_data = current_stripe + src * element_stride * num_comps; + memcpy(dest_ptr, src_data, num_comps * sizeof(float)); + dest_ptr += num_comps; } for (; src < stripe_size + scratch->pad_right; src++) { - const byte *src_data = current_stripe + (src - stripe_size) * element_stride * 4; - decode_srgba((float*)(dest_ptr++), src_data); + const float *src_data = current_stripe + (src - stripe_size) * element_stride * num_comps; + memcpy(dest_ptr, src_data, num_comps * sizeof(float)); + dest_ptr += num_comps; } } else @@ -586,8 +576,9 @@ static void filterscratch_fill_from_image(struct filterscratch_s *scratch, byte src += stripe_size; for (int i = 0; i < scratch->pad_left + stripe_size + scratch->pad_right; i++) { - const byte *src_data = current_stripe + src * element_stride * 4; - decode_srgba((float*)(dest_ptr++), src_data); + const float *src_data = current_stripe + src * element_stride * num_comps; + memcpy(dest_ptr, src_data, num_comps * sizeof(float)); + dest_ptr += scratch->num_comps; src = (src + 1) % stripe_size; } } @@ -596,41 +587,46 @@ static void filterscratch_fill_from_image(struct filterscratch_s *scratch, byte /* Apply a (separable) filter along one dimension of an image. * Whether this is done along the X or Y dimension depends on the "stripe size" * and "stripe stride" options. See filter_image() for how to use it practically. */ -static void filter_one_dimension(byte* pixels, const float kernel[], unsigned kernel_size, - int stripe_size, int num_stripes, int stripe_stride, int element_stride) +static void filter_one_dimension_float(float* pixels, int num_comps, + const float kernel[], unsigned kernel_size, + int stripe_size, int num_stripes, + int stripe_stride, int element_stride) { struct filterscratch_s scratch; - filterscratch_init(&scratch, kernel_size, stripe_size); - byte *current_stripe = pixels; + filterscratch_init(&scratch, kernel_size, stripe_size, num_comps); + float *current_stripe = pixels; + float* values = alloca(num_comps * sizeof(float)); for (int s = 0; s < num_stripes; s++) { // back up image data to scratch buffer - filterscratch_fill_from_image(&scratch, current_stripe, stripe_size, element_stride); + filterscratch_fill_from_float_image(&scratch, current_stripe, stripe_size, element_stride); // filter the stripe for (int i = 0; i < stripe_size; i++) { - vec4_t color; - memset(color, 0, sizeof(color)); + memset(values, 0, num_comps * sizeof(float)); for (int j = 0; j < kernel_size; j++) { float f = kernel[j]; - vec4_t *src_p = scratch.ptr + i + j; - Vector4MA(color, f, *src_p, color); + float *src_p = scratch.ptr + (i + j) * num_comps; + for (int c = 0; c < num_comps; c++) + { + values[c] += f * src_p[c]; + } } - encode_srgba(current_stripe + i * element_stride * 4, color); + memcpy(current_stripe + i * element_stride * num_comps, values, num_comps * sizeof(float)); } - current_stripe += stripe_stride * 4; + current_stripe += stripe_stride * num_comps; } filterscratch_free(&scratch); } // Apply a (separable) filter to an image. -static void filter_image(byte* pixels, const float kernel[], unsigned kernel_size, int width, int height) +static void filter_float_image(float* pixels, int num_comps, const float kernel[], unsigned kernel_size, int width, int height) { // Filter horizontally - filter_one_dimension(pixels, kernel, kernel_size, width, height, width, 1); + filter_one_dimension_float(pixels, num_comps, kernel, kernel_size, width, height, width, 1); // Filter vertically - filter_one_dimension(pixels, kernel, kernel_size, height, width, 1, width); + filter_one_dimension_float(pixels, num_comps, kernel, kernel_size, height, width, 1, width); } // Fake an emissive texture from a diffuse texture by using pixels brighter than a certain amount @@ -639,67 +635,79 @@ static void apply_fake_emissive_threshold(image_t *image) int w = image->upload_width; int h = image->upload_height; - byte* current_pixel = image->pix_data; - + float *bright_mask = IMG_AllocPixels(w * h * sizeof(float)); + + /* Extract "bright" pixels by choosing all those that have one component + larger than some threshold. + This value was choses b/c "bright" pixels in Q2 have at least one component with value 215 */ + byte bright_threshold = 215; + + float *current_bright_mask = bright_mask; + byte *src_pixel = image->pix_data; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + byte max_comp = max(src_pixel[0], src_pixel[1]); + max_comp = max(src_pixel[2], max_comp); + if (max_comp < bright_threshold) { + *current_bright_mask = 0; + } else { + *current_bright_mask = LUMINANCE(decode_srgb(src_pixel[0]), decode_srgb(src_pixel[1]), decode_srgb(src_pixel[2])); + } + current_bright_mask++; + src_pixel += 4; + } + } + + // Blur those "bright" pixels + const float filter[] = { 0.0093, 0.028002, 0.065984, 0.121703, 0.175713, 0.198596, 0.175713, 0.121703, 0.065984, 0.028002, 0.0093 }; + filter_float_image(bright_mask, 1, filter, sizeof(filter) / sizeof(filter[0]), w, h); + + // Do a pass to find max luminance of bright_mask... + current_bright_mask = bright_mask; float max_lum = 0; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { - vec3_t color; - color[0] = decode_srgb(current_pixel[0]); - color[1] = decode_srgb(current_pixel[1]); - color[2] = decode_srgb(current_pixel[2]); - float lum = LUMINANCE(color[0], color[1], color[2]); + float lum = *current_bright_mask; if (lum > max_lum) max_lum = lum; - current_pixel += 4; + current_bright_mask++; } } + // ...and use it to normalize max luminance to 1 + float lum_scale = max_lum > 0 ? 1.0f / max_lum : 1.0f; - float threshold = max_lum * 0.68f; // FIXME: Empirical guess. Perhaps decide using some more clever algorithm? - - current_pixel = image->pix_data; + // Combine blurred "bright" mask with original image (to retain some colorization) + current_bright_mask = bright_mask; + byte *current_img_pixel = image->pix_data; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { - vec3_t color; - color[0] = decode_srgb(current_pixel[0]); - color[1] = decode_srgb(current_pixel[1]); - color[2] = decode_srgb(current_pixel[2]); - float lum = LUMINANCE(color[0], color[1], color[2]); - if(lum < threshold) - { - current_pixel[0] = 0; - current_pixel[1] = 0; - current_pixel[2] = 0; - } - else - { - float new_lum = (lum - threshold) / (max_lum - threshold); - VectorScale(color, new_lum / lum, color); - current_pixel[0] = encode_srgb(color[0]); - current_pixel[1] = encode_srgb(color[1]); - current_pixel[2] = encode_srgb(color[2]); - } - - current_pixel += 4; + vec3_t color_img; + color_img[0] = decode_srgb(current_img_pixel[0]); + color_img[1] = decode_srgb(current_img_pixel[1]); + color_img[2] = decode_srgb(current_img_pixel[2]); + + /* The formula for the "emissive" color is objectively weird, + but is subjectively suitable for typical "light" textures... + It keeps the "light" pixels but avoids bleeding from neighbouring + "other" pixels */ + float src_lum = LUMINANCE(color_img[0], color_img[1], color_img[2]); + src_lum *= src_lum; + float scale = *current_bright_mask * src_lum * lum_scale; + color_img[0] *= scale; + color_img[1] *= scale; + color_img[2] *= scale; + + current_img_pixel[0] = encode_srgb(color_img[0]); + current_img_pixel[1] = encode_srgb(color_img[1]); + current_img_pixel[2] = encode_srgb(color_img[2]); + + current_bright_mask++; + current_img_pixel += 4; } } - // Scale up - this makes low-res textures look bit better, together with the blur below - int new_width = w * 2; - int new_height = h * 2; - int new_size = new_width * new_height * 4; - byte *new_data = IMG_AllocPixels(new_size); - IMG_ResampleTexture(image->pix_data, w, h, new_data, new_width, new_height); - byte *old_data = image->pix_data; - image->pix_data = new_data; - Z_Free(old_data); - w = image->upload_width = new_width; - h = image->upload_height = new_height; - - // Apply a blur - const float filter[] = { 0.00598f, 0.060626f, 0.241843f, 0.383103f, 0.241843f, 0.060626f, 0.00598f }; - filter_image(image->pix_data, filter, sizeof(filter) / sizeof(filter[0]), w, h); + Z_Free(bright_mask); } image_t *vkpt_fake_emissive_texture(image_t *image) From 48ae8e9539d8d3239ca0b9f3619c636f03d2d9f0 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Thu, 3 Jun 2021 14:09:47 +0200 Subject: [PATCH 16/19] Add 2x upsampling and mild filter pass to generated emissive texture Makes them look a bit less blocky. --- src/refresh/vkpt/textures.c | 157 +++++++++++++++++++++++++++++++++--- 1 file changed, 148 insertions(+), 9 deletions(-) diff --git a/src/refresh/vkpt/textures.c b/src/refresh/vkpt/textures.c index b9844b930..46bec8160 100644 --- a/src/refresh/vkpt/textures.c +++ b/src/refresh/vkpt/textures.c @@ -629,6 +629,102 @@ static void filter_float_image(float* pixels, int num_comps, const float kernel[ filter_one_dimension_float(pixels, num_comps, kernel, kernel_size, height, width, 1, width); } +struct bilerp_s +{ + int current_output_row; + float *current_input_data; + float *next_input_data; + float *output_data; +}; + +static void bilerp_init(struct bilerp_s* bilerp, int input_w) +{ + bilerp->current_output_row = -1; + bilerp->current_input_data = IMG_AllocPixels(input_w * sizeof(float) * 3); + bilerp->next_input_data = IMG_AllocPixels(input_w * sizeof(float) * 3); + bilerp->output_data = IMG_AllocPixels(input_w * 2 * sizeof(float) * 3); +} + +static void bilerp_free(struct bilerp_s* bilerp) +{ + Z_Free(bilerp->current_input_data); + Z_Free(bilerp->next_input_data); + Z_Free(bilerp->output_data); +} + +static inline void _bilerp_get_next_output_line(struct bilerp_s *bilerp, const float** output_line, const float* next_input, int input_w) +{ + if(bilerp->current_output_row == -1) { + memcpy(bilerp->next_input_data, next_input, input_w * sizeof(float) * 3); + bilerp->current_output_row = 0; + } + + if((bilerp->current_output_row & 1) == 0) { + // Even output line: use input lines + // Swap next_input_data into current_input_data + float *tmp = bilerp->next_input_data; + bilerp->next_input_data = bilerp->current_input_data; + bilerp->current_input_data = tmp; + } else { + // Odd output line: interpolate between input lines + float *color_dest = bilerp->next_input_data; + memcpy(bilerp->next_input_data, next_input, input_w * sizeof(float) * 3); + + float *color_ptr = bilerp->current_input_data; + float *next_color_ptr = bilerp->next_input_data; + for (int x = 0; x < input_w; x++) { + color_ptr[0] = (color_ptr[0] + next_color_ptr[0]) * 0.5f; + color_ptr[1] = (color_ptr[1] + next_color_ptr[1]) * 0.5f; + color_ptr[2] = (color_ptr[2] + next_color_ptr[2]) * 0.5f; + color_ptr += 3; + next_color_ptr += 3; + } + } + + float *out_ptr = bilerp->output_data; + float color[3]; + const float* color_ptr = bilerp->current_input_data; + for (int out_x = 0; out_x < input_w * 2 - 1; out_x++) { + if((out_x & 1) == 0) { + // Even output row: direct value + memcpy(color, color_ptr, 3 * sizeof(float)); + } else { + // Odd output row: interpolate between colors + color_ptr += 3; + color[0] = (color[0] + color_ptr[0]) * 0.5f; + color[1] = (color[1] + color_ptr[1]) * 0.5f; + color[2] = (color[2] + color_ptr[2]) * 0.5f; + } + memcpy(out_ptr, color, 3 * sizeof(float)); + out_ptr += 3; + } + + // Last row: interpolate between last and first pixel + color[0] = (color_ptr[0] + bilerp->current_input_data[0]) * 0.5f; + color[1] = (color_ptr[1] + bilerp->current_input_data[1]) * 0.5f; + color[2] = (color_ptr[2] + bilerp->current_input_data[2]) * 0.5f; + memcpy(out_ptr, color, 3 * sizeof(float)); + + *output_line = bilerp->output_data; + + bilerp->current_output_row++; +} + +static inline void bilerp_get_next_output_line_from_rgb_f32(struct bilerp_s *bilerp, const float** output_line, const float* input_data, int input_w, int input_h) +{ + const float *next_input = NULL; + if (bilerp->current_output_row == -1) { + next_input = input_data; + } else if ((bilerp->current_output_row & 1) != 0) { + int in_y = (bilerp->current_output_row + 1) >> 1; + // Wraparound last line + if(in_y >= input_h) + in_y = 0; + next_input = input_data + in_y * input_w * 3; + } + _bilerp_get_next_output_line(bilerp, output_line, next_input, input_w); +} + // Fake an emissive texture from a diffuse texture by using pixels brighter than a certain amount static void apply_fake_emissive_threshold(image_t *image) { @@ -677,7 +773,11 @@ static void apply_fake_emissive_threshold(image_t *image) // ...and use it to normalize max luminance to 1 float lum_scale = max_lum > 0 ? 1.0f / max_lum : 1.0f; - // Combine blurred "bright" mask with original image (to retain some colorization) + /* Combine blurred "bright" mask with original image (to retain some colorization). + Produce float output for upsampling pass */ + float *final = IMG_AllocPixels(w * h * 3 * sizeof(float)); + + float *out_final = final; current_bright_mask = bright_mask; byte *current_img_pixel = image->pix_data; for (int y = 0; y < h; y++) { @@ -694,20 +794,59 @@ static void apply_fake_emissive_threshold(image_t *image) float src_lum = LUMINANCE(color_img[0], color_img[1], color_img[2]); src_lum *= src_lum; float scale = *current_bright_mask * src_lum * lum_scale; - color_img[0] *= scale; - color_img[1] *= scale; - color_img[2] *= scale; - - current_img_pixel[0] = encode_srgb(color_img[0]); - current_img_pixel[1] = encode_srgb(color_img[1]); - current_img_pixel[2] = encode_srgb(color_img[2]); + out_final[0] = color_img[0] * scale; + out_final[1] = color_img[1] * scale; + out_final[2] = color_img[2] * scale; + out_final += 3; current_bright_mask++; current_img_pixel += 4; } } - Z_Free(bright_mask); + + // Interpolate final image to 2x size, apply a mild filter, to have it look less blocky + int width_2x = w * 2; + int height_2x = h * 2; + float *final_2x = IMG_AllocPixels(width_2x * height_2x * 3 * sizeof(float)); + + struct bilerp_s bilerp_final; + bilerp_init(&bilerp_final, w); + float *out_final_2x = final_2x; + for (int out_y = 0; out_y < height_2x; out_y++) { + float *img_line; + bilerp_get_next_output_line_from_rgb_f32(&bilerp_final, &img_line, final, w, h); + memcpy(out_final_2x, img_line, width_2x * 3 * sizeof(float)); + out_final_2x += width_2x * 3; + } + bilerp_free(&bilerp_final); + Z_Free(final); + + const float filter_final[] = { 0.157731, 0.684538, 0.157731 }; + filter_float_image(final_2x, 3, filter_final, sizeof(filter_final) / sizeof(filter_final[0]), width_2x, height_2x); + + // Final -> SRGB + int new_size = width_2x * height_2x * 4; + Z_Free(image->pix_data); + image->pix_data = IMG_AllocPixels(new_size); + image->upload_width = width_2x; + image->upload_height = height_2x; + + float* current_pixel = final_2x; + byte *out_pixel = image->pix_data; + for (int y = 0; y < height_2x; y++) { + for (int x = 0; x < width_2x; x++) { + out_pixel[0] = encode_srgb(current_pixel[0]); + out_pixel[1] = encode_srgb(current_pixel[1]); + out_pixel[2] = encode_srgb(current_pixel[2]); + out_pixel[3] = 255; + + current_pixel += 3; + out_pixel += 4; + } + } + + Z_Free(final_2x); } image_t *vkpt_fake_emissive_texture(image_t *image) From 937fb7757f622155b88c1edafa1252b29750cf9d Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Sun, 25 Jul 2021 23:17:28 +0200 Subject: [PATCH 17/19] Change image loading logic to not prefer 'game' images if they're identical to the 'baseq2' image --- src/refresh/images.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/refresh/images.c b/src/refresh/images.c index 4172409ad..b1a2b2cb9 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -804,6 +804,24 @@ static int _try_image_format(imageformat_t fmt, image_t *image, int try_src, byt if (!data) { return len; } + /* Don't prefer game image if it's identical to the base version + Some games (eg rogue) ship image assets that are identical to the + baseq2 version. + If that is the case, prefer the baseq2 copy - because those may have + override image and additional material images! + */ + if (try_src == TRY_IMAGE_SRC_GAME) { + byte *data_base; + ssize_t len_base; + len_base = FS_LoadFileFlags(image->name, (void **)&data_base, FS_PATH_BASE); + if((len == len_base) && (memcmp(data, data_base, len) == 0)) { + // Identical data in game, pretend file doesn't exist + FS_FreeFile(data); + FS_FreeFile(data_base); + return Q_ERR_NOENT; + } + FS_FreeFile(data_base); + } // decompress the image ret = img_loaders[fmt].load(data, len, image, pic); From 72becb3e09809f121252d9605f63cec92125a8d9 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Mon, 26 Jul 2021 08:50:02 +0200 Subject: [PATCH 18/19] Remove a redundant piece of code --- src/refresh/images.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index b1a2b2cb9..787ec142e 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1008,12 +1008,6 @@ load_img(const char *name, image_t *image) get_image_dimensions(fmt, image); } - // if we are replacing 8-bit texture with a higher resolution 32-bit - // texture, we need to recover original image dimensions - if (fmt <= IM_WAL && ret > IM_WAL) { - get_image_dimensions(fmt, image); - } - if (ret < 0) { memset(image, 0, sizeof(*image)); return ret; From cf65b63b5d3a1927565e89933c18cc23cb408811 Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Mon, 26 Jul 2021 08:51:27 +0200 Subject: [PATCH 19/19] Tweak image game vs base behaviour for 'overidden' images When looking for an override, don't prefer 'game' images over 'base' images. This is more or less another workaround for games that duplicate base textures; in that case, we want to preferably use any high-res overrides and it's additional images. --- src/refresh/images.c | 184 +++++++++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 87 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index 787ec142e..6f4f6ec32 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1020,6 +1020,74 @@ load_img(const char *name, image_t *image) return Q_ERR_SUCCESS; } +// Try to load an image, possibly with an alternative extension +static qerror_t try_load_image_candidate(image_t *image, const char *orig_name, size_t orig_len, byte **pic_p, imagetype_t type, imageflags_t flags, qboolean ignore_extension, int try_location) +{ + qerror_t ret; + + image->type = type; + image->flags = flags; + image->registration_sequence = registration_sequence; + + // find out original extension + imageformat_t fmt; + for (fmt = 0; fmt < IM_MAX; fmt++) + { + if (!Q_stricmp(image->name + image->baselen + 1, img_loaders[fmt].ext)) + { + break; + } + } + + // load the pic from disk + *pic_p = NULL; + + if (fmt == IM_MAX) + { + // unknown extension, but give it a chance to load anyway + ret = try_other_formats(IM_MAX, image, try_location, pic_p); + if (ret == Q_ERR_NOENT) + { + // not found, change error to invalid path + ret = Q_ERR_INVALID_PATH; + } + } + else if (ignore_extension) + { + // forcibly replace the extension + ret = try_other_formats(IM_MAX, image, try_location, pic_p); + } + else + { + // first try with original extension + ret = _try_image_format(fmt, image, try_location, pic_p); + if (ret == Q_ERR_NOENT) + { + // retry with remaining extensions + ret = try_other_formats(fmt, image, try_location, pic_p); + } + } + + // record last modified time (skips reload when invoking IMG_ReloadAll) + image->last_modified = 0; + FS_LastModified(image->name, &image->last_modified); + + // Restore original name if it was overridden + if(orig_name) { + memcpy(image->name, orig_name, orig_len + 1); + image->baselen = orig_len - 4; + } + + // if we are replacing 8-bit texture with a higher resolution 32-bit + // texture, we need to recover original image dimensions + if (fmt <= IM_WAL && ret > IM_WAL) + { + get_image_dimensions(fmt, image); + } + + return ret; +} + // finds or loads the given image, adding it to the hash table. static qerror_t find_or_load_image(const char *name, size_t len, imagetype_t type, imageflags_t flags, @@ -1028,7 +1096,6 @@ static qerror_t find_or_load_image(const char *name, size_t len, image_t *image; byte *pic; unsigned hash; - imageformat_t fmt; qerror_t ret; *image_p = NULL; @@ -1061,100 +1128,43 @@ static qerror_t find_or_load_image(const char *name, size_t len, if (!vid_rtx->integer && (type != IT_PIC)) override_textures = 0; - // Always prefer images from the game dir, even if format might be 'inferior' - for (int try_location = Q_stricmp(fs_game->string, BASEGAME) ? TRY_IMAGE_SRC_GAME : TRY_IMAGE_SRC_BASE; - try_location >= TRY_IMAGE_SRC_BASE; - try_location--) + if(override_textures) { - int location_flag = try_location == TRY_IMAGE_SRC_GAME ? IF_SRC_GAME : IF_SRC_MASK; - if(((flags & IF_SRC_MASK) != 0) && ((flags & IF_SRC_MASK) != location_flag)) - continue; - - for (int use_override = override_textures; use_override >= 0; use_override--) - { - // fill in some basic info - if (use_override) - { - const char *last_slash = strrchr(name, '/'); - if (!last_slash) - last_slash = name; - else - last_slash += 1; - - strcpy(image->name, "overrides/"); - strcat(image->name, last_slash); - image->baselen = strlen(image->name) - 4; - } - else - { - memcpy(image->name, name, len + 1); - image->baselen = len - 4; - } - image->type = type; - image->flags = flags | location_flag; - image->registration_sequence = registration_sequence; - - // find out original extension - for (fmt = 0; fmt < IM_MAX; fmt++) - { - if (!Q_stricmp(image->name + image->baselen + 1, img_loaders[fmt].ext)) - { - break; - } - } + const char *last_slash = strrchr(name, '/'); + if (!last_slash) + last_slash = name; + else + last_slash += 1; - // load the pic from disk - pic = NULL; - - if (fmt == IM_MAX) - { - // unknown extension, but give it a chance to load anyway - ret = try_other_formats(IM_MAX, image, try_location, &pic); - if (ret == Q_ERR_NOENT) - { - // not found, change error to invalid path - ret = Q_ERR_INVALID_PATH; - } - } - else if (override_textures) - { - // forcibly replace the extension - ret = try_other_formats(IM_MAX, image, try_location, &pic); - } - else - { - // first try with original extension - ret = _try_image_format(fmt, image, try_location, &pic); - if (ret == Q_ERR_NOENT) - { - // retry with remaining extensions - ret = try_other_formats(fmt, image, try_location, &pic); - } - } - - // record last modified time (skips reload when invoking IMG_ReloadAll) - image->last_modified = 0; - FS_LastModified(image->name, &image->last_modified); + strcpy(image->name, "overrides/"); + strcat(image->name, last_slash); + image->baselen = strlen(image->name) - 4; + ret = try_load_image_candidate(image, name, len, &pic, type, flags, qtrue, -1); + memcpy(image->name, name, len + 1); + image->baselen = len - 4; + } - if (use_override) - { - memcpy(image->name, name, len + 1); - image->baselen = len - 4; - } + // Try non-overridden image + if (ret < 0) + { + // Always prefer images from the game dir, even if format might be 'inferior' + for (int try_location = Q_stricmp(fs_game->string, BASEGAME) ? TRY_IMAGE_SRC_GAME : TRY_IMAGE_SRC_BASE; + try_location >= TRY_IMAGE_SRC_BASE; + try_location--) + { + int location_flag = try_location == TRY_IMAGE_SRC_GAME ? IF_SRC_GAME : IF_SRC_MASK; + if(((flags & IF_SRC_MASK) != 0) && ((flags & IF_SRC_MASK) != location_flag)) + continue; - // if we are replacing 8-bit texture with a higher resolution 32-bit - // texture, we need to recover original image dimensions - if (fmt <= IM_WAL && ret > IM_WAL) - { - get_image_dimensions(fmt, image); - } + // fill in some basic info + memcpy(image->name, name, len + 1); + image->baselen = len - 4; + ret = try_load_image_candidate(image, NULL, 0, &pic, type, flags, !!override_textures, try_location); + image->flags |= location_flag; if (ret >= 0) break; } - - if (ret >= 0) - break; } if (ret < 0) {