From ccf560b91b874ca7978eea86f006cbd3fefb3119 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 12 Sep 2024 01:00:03 +0300 Subject: [PATCH 01/43] Add command to play next track. --- doc/client.asciidoc | 3 ++- src/client/sound/ogg.c | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index abbcff7a9..579b45891 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -1593,11 +1593,12 @@ remotemode
:: Put client console into rcon mode. All commands entered will be forwarded to remove server. Press Ctrl+D or close console to exit this mode. -ogg :: +ogg :: Execute OGG subcommand. Available subcommands: info::: Display information about currently playing background music track. play ::: Start playing background music track ‘music/_track_.ogg’. stop::: Stop playing background music track. + next::: Play next track if shuffling is enabled. whereis [all]:: Search for _path_ and print the name of packfile or directory where it is diff --git a/src/client/sound/ogg.c b/src/client/sound/ogg.c index d07cb2d17..4700a8a21 100644 --- a/src/client/sound/ogg.c +++ b/src/client/sound/ogg.c @@ -709,6 +709,7 @@ static void OGG_Cmd_c(genctx_t *ctx, int argnum) Prompt_AddMatch(ctx, "info"); Prompt_AddMatch(ctx, "play"); Prompt_AddMatch(ctx, "stop"); + Prompt_AddMatch(ctx, "next"); return; } @@ -726,8 +727,10 @@ static void OGG_Cmd_f(void) OGG_Play_f(); else if (!strcmp(cmd, "stop")) OGG_Stop(); + else if (!strcmp(cmd, "next")) + OGG_Play(); else - Com_Printf("Usage: %s \n", Cmd_Argv(0)); + Com_Printf("Usage: %s \n", Cmd_Argv(0)); } static void ogg_enable_changed(cvar_t *self) From 47656f88fed5cac9b395992cc0d9a63accd593d8 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 12 Sep 2024 01:01:58 +0300 Subject: [PATCH 02/43] Add support for cubemap skies. Support loading cubemap textures for SURF_SKY faces. This allows multiple skybox textures per map. Also rewrite N64 skies to not use skybox drawing code at all. Also fully support skies on bmodels, but only enable them in DECOUPLED_LM maps. --- doc/client.asciidoc | 13 +-- inc/refresh/refresh.h | 1 + src/refresh/gl.h | 49 +++++----- src/refresh/images.c | 33 ++++--- src/refresh/images.h | 5 +- src/refresh/main.c | 6 +- src/refresh/mesh.c | 4 +- src/refresh/models.c | 3 - src/refresh/shader.c | 39 +++++--- src/refresh/sky.c | 115 ++++++++++++++++++++---- src/refresh/state.c | 32 +++++++ src/refresh/surf.c | 46 +++++++--- src/refresh/tess.c | 28 ++++-- src/refresh/texture.c | 203 +++++++++++++++++++++++++++++++++++++++--- src/refresh/world.c | 11 ++- 15 files changed, 481 insertions(+), 107 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 579b45891..120be0ed6 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -674,11 +674,14 @@ gl_downsample_skins:: Default value is 1 (downsampling enabled). gl_drawsky:: - Enables skybox texturing and N64 skies. N64 skies only work if - ‘gl_shaders’ is enabled. Default value is 1. - - 0 — draw solid black sky - - 1 — draw normal sky, or N64 sky if present - - 2 — draw normal sky, ignore N64 sky + Enables skybox texturing. 0 means to draw sky in solid black color. + Default value is 1 (enabled). + +gl_cubemaps:: + Enables use of cubemaps for skybox drawing. Cubemaps allow multiple skybox + textures per map, and help to avoid sky rendering bugs. Also enables N64 + skies (although these aren't technically cubemaps). Only effective if + ‘gl_shaders’ is enabled. Default value is 1 (enabled). gl_waterwarp:: Enable screen warping effect when underwater. Only effective when using diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 8e0be95f1..d2c6617c9 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -155,6 +155,7 @@ typedef enum { IF_NEAREST = BIT(7), // don't bilerp IF_OPAQUE = BIT(8), // known to be opaque IF_DEFAULT_FLARE = BIT(9), // default flare hack + IF_CUBEMAP = BIT(10), // cubemap (or part of it) // not stored in image IF_OPTIONAL = BIT(16), // don't warn if not found diff --git a/src/refresh/gl.h b/src/refresh/gl.h index f5018aa05..e302eb6b2 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -50,7 +50,7 @@ typedef GLuint glIndex_t; #define TAB_SIN(x) gl_static.sintab[(x) & 255] #define TAB_COS(x) gl_static.sintab[((x) + 64) & 255] -#define NUM_AUTO_TEXTURES 7 +#define NUM_AUTO_TEXTURES 9 typedef struct { GLuint query; @@ -63,6 +63,8 @@ typedef struct { typedef struct { bool registering; bool use_shaders; + bool use_cubemaps; + bool use_bmodel_skies; struct { bsp_t *cache; vec_t *vertices; @@ -90,7 +92,6 @@ typedef struct { byte lightstylemap[MAX_LIGHTSTYLES]; hash_map_t *queries; hash_map_t *programs; - image_t *classic_sky; } glStatic_t; typedef struct { @@ -111,6 +112,7 @@ typedef struct { float entscale; vec3_t entaxis[3]; GLfloat entmatrix[16]; + GLfloat skymatrix[2][16]; lightpoint_t lightpoint; int num_beams; int num_flares; @@ -245,7 +247,7 @@ bool GL_AllocBlock(int width, int height, uint16_t *inuse, void GL_MultMatrix(GLfloat *restrict out, const GLfloat *restrict a, const GLfloat *restrict b); void GL_SetEntityAxis(void); void GL_RotationMatrix(GLfloat *matrix); -void GL_RotateForEntity(void); +void GL_RotateForEntity(bool skies); void GL_ClearErrors(void); bool GL_ShowErrors(const char *func); @@ -471,19 +473,21 @@ typedef enum { GLS_INTENSITY_ENABLE = BIT(11), GLS_GLOWMAP_ENABLE = BIT(12), GLS_CLASSIC_SKY = BIT(13), - GLS_DEFAULT_FLARE = BIT(14), + GLS_DEFAULT_SKY = BIT(14), + GLS_DEFAULT_FLARE = BIT(15), - GLS_SHADE_SMOOTH = BIT(15), - GLS_SCROLL_X = BIT(16), - GLS_SCROLL_Y = BIT(17), - GLS_SCROLL_FLIP = BIT(18), - GLS_SCROLL_SLOW = BIT(19), + GLS_SHADE_SMOOTH = BIT(16), + GLS_SCROLL_X = BIT(17), + GLS_SCROLL_Y = BIT(18), + GLS_SCROLL_FLIP = BIT(19), + GLS_SCROLL_SLOW = BIT(20), GLS_BLEND_MASK = GLS_BLEND_BLEND | GLS_BLEND_ADD | GLS_BLEND_MODULATE, GLS_COMMON_MASK = GLS_DEPTHMASK_FALSE | GLS_DEPTHTEST_DISABLE | GLS_CULL_DISABLE | GLS_BLEND_MASK, + GLS_SKY_MASK = GLS_CLASSIC_SKY | GLS_DEFAULT_SKY, GLS_SHADER_MASK = GLS_ALPHATEST_ENABLE | GLS_TEXTURE_REPLACE | GLS_SCROLL_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_WARP_ENABLE | GLS_INTENSITY_ENABLE | GLS_GLOWMAP_ENABLE | - GLS_CLASSIC_SKY | GLS_DEFAULT_FLARE, + GLS_SKY_MASK | GLS_DEFAULT_FLARE, GLS_SCROLL_MASK = GLS_SCROLL_ENABLE | GLS_SCROLL_X | GLS_SCROLL_Y | GLS_SCROLL_FLIP | GLS_SCROLL_SLOW, } glStateBits_t; @@ -529,6 +533,7 @@ typedef enum { typedef struct { GLfloat mvp[16]; + GLfloat msky[2][16]; GLfloat time; GLfloat modulate; GLfloat add; @@ -538,14 +543,13 @@ typedef struct { GLfloat w_amp[2]; GLfloat w_phase[2]; GLfloat scroll[2]; - GLfloat vieworg[3]; - GLfloat pad_2; } glUniformBlock_t; typedef struct { glTmu_t client_tmu; glTmu_t server_tmu; GLuint texnums[MAX_TMUS]; + GLuint texnumcube; glStateBits_t state_bits; glArrayBits_t array_bits; GLuint currentbuffer[2]; @@ -675,6 +679,8 @@ typedef enum { void GL_ForceTexture(glTmu_t tmu, GLuint texnum); void GL_BindTexture(glTmu_t tmu, GLuint texnum); +void GL_ForceCubemap(GLuint texnum); +void GL_BindCubemap(GLuint texnum); void GL_CommonStateBits(glStateBits_t bits); void GL_ScrollPos(vec2_t scroll, glStateBits_t bits); void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed); @@ -715,13 +721,15 @@ void GL_Blend(void); */ // auto textures -#define TEXNUM_DEFAULT gl_static.texnums[0] -#define TEXNUM_SCRAP gl_static.texnums[1] -#define TEXNUM_PARTICLE gl_static.texnums[2] -#define TEXNUM_BEAM gl_static.texnums[3] -#define TEXNUM_WHITE gl_static.texnums[4] -#define TEXNUM_BLACK gl_static.texnums[5] -#define TEXNUM_RAW gl_static.texnums[6] +#define TEXNUM_DEFAULT gl_static.texnums[0] +#define TEXNUM_SCRAP gl_static.texnums[1] +#define TEXNUM_PARTICLE gl_static.texnums[2] +#define TEXNUM_BEAM gl_static.texnums[3] +#define TEXNUM_WHITE gl_static.texnums[4] +#define TEXNUM_BLACK gl_static.texnums[5] +#define TEXNUM_RAW gl_static.texnums[6] +#define TEXNUM_CUBEMAP_DEFAULT gl_static.texnums[7] +#define TEXNUM_CUBEMAP_BLACK gl_static.texnums[8] void Scrap_Upload(void); @@ -732,8 +740,6 @@ bool GL_InitWarpTexture(void); extern cvar_t *gl_intensity; -extern image_t shell_texture; - /* * gl_tess.c * @@ -788,6 +794,7 @@ void GL_LightPoint(const vec3_t origin, vec3_t color); void R_AddSkySurface(const mface_t *surf); void R_ClearSkyBox(void); void R_DrawSkyBox(void); +void R_RotateForSky(void); void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis); /* diff --git a/src/refresh/images.c b/src/refresh/images.c index 55483fe65..e6d6f83e7 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1557,7 +1557,7 @@ static void IMG_List_f(void) Com_Printf("------------------\n"); texels = count = 0; - for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { + for (i = R_NUM_AUTO_IMG, image = r_images + i; i < r_numImages; i++, image++) { if (!image->name[0]) continue; if (mask && !(mask & BIT(image->type))) @@ -1595,7 +1595,7 @@ static image_t *alloc_image(void) image_t *image, *placeholder = NULL; // find a free image_t slot - for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { + for (i = R_NUM_AUTO_IMG, image = r_images + i; i < r_numImages; i++, image++) { if (!image->name[0]) return image; if (!image->upload_width && !image->upload_height && !placeholder) @@ -2030,14 +2030,27 @@ static image_t *find_or_load_image(const char *name, size_t len, image_t *IMG_Find(const char *name, imagetype_t type, imageflags_t flags) { + char buffer[MAX_QPATH]; image_t *image; + size_t len; Q_assert(name); - if ((image = find_or_load_image(name, strlen(name), type, flags))) - return image; + // path MUST never overflow + len = FS_NormalizePathBuffer(buffer, name, sizeof(buffer)); + image = find_or_load_image(buffer, len, type, flags); - return R_NOTEXTURE; + // missing (or invalid) sky texture will use default sky + if (type == IT_SKY) { + if (!image) + return R_SKYTEXTURE; + if (~image->flags & flags & IF_CUBEMAP) + return R_SKYTEXTURE; + } + + if (!image) + return R_NOTEXTURE; + return image; } /* @@ -2125,7 +2138,7 @@ void IMG_FreeUnused(void) image_t *image; int i, count = 0; - for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { + for (i = R_NUM_AUTO_IMG, image = r_images + i; i < r_numImages; i++, image++) { if (!image->name[0]) continue; // free image_t slot if (image->registration_sequence == r_registration_sequence) @@ -2152,7 +2165,7 @@ void IMG_FreeAll(void) image_t *image; int i, count = 0; - for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { + for (i = R_NUM_AUTO_IMG, image = r_images + i; i < r_numImages; i++, image++) { if (!image->name[0]) continue; // free image_t slot // free it @@ -2169,7 +2182,7 @@ void IMG_FreeAll(void) List_Init(&r_imageHash[i]); // &r_images[0] == R_NOTEXTURE - r_numImages = 1; + r_numImages = R_NUM_AUTO_IMG; } /* @@ -2260,12 +2273,12 @@ void IMG_Init(void) List_Init(&r_imageHash[i]); // &r_images[0] == R_NOTEXTURE - r_numImages = 1; + r_numImages = R_NUM_AUTO_IMG; } void IMG_Shutdown(void) { Cmd_Deregister(img_cmd); - memset(r_images, 0, sizeof(r_images[0])); // clear R_NOTEXTURE + memset(r_images, 0, R_NUM_AUTO_IMG * sizeof(r_images[0])); // clear R_NOTEXTURE r_numImages = 0; } diff --git a/src/refresh/images.h b/src/refresh/images.h index 889258266..c1646b5da 100644 --- a/src/refresh/images.h +++ b/src/refresh/images.h @@ -76,7 +76,10 @@ extern int r_numImages; extern unsigned r_registration_sequence; -#define R_NOTEXTURE &r_images[0] +#define R_NUM_AUTO_IMG 3 +#define R_NOTEXTURE (&r_images[0]) +#define R_SHELLTEXTURE (&r_images[1]) +#define R_SKYTEXTURE (&r_images[2]) extern uint32_t d_8to24table[256]; diff --git a/src/refresh/main.c b/src/refresh/main.c index e87772a63..0b70faef3 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -311,11 +311,15 @@ void GL_RotationMatrix(GLfloat *matrix) matrix[15] = 1; } -void GL_RotateForEntity(void) +void GL_RotateForEntity(bool skies) { GLfloat matrix[16]; GL_RotationMatrix(matrix); + if (skies) { + GL_MultMatrix(gls.u_block.msky[0], glr.skymatrix[0], matrix); + GL_MultMatrix(gls.u_block.msky[1], glr.skymatrix[1], matrix); + } GL_MultMatrix(glr.entmatrix, glr.viewmatrix, matrix); GL_ForceMatrix(glr.entmatrix); } diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index b4bf25176..64dd23f62 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -570,7 +570,7 @@ static const image_t *skin_for_mesh(image_t **skins, int num_skins) const entity_t *ent = glr.ent; if (ent->flags & RF_SHELL_MASK) - return &shell_texture; + return R_SHELLTEXTURE; if (ent->skin) return IMG_ForHandle(ent->skin); @@ -880,7 +880,7 @@ void GL_DrawAliasModel(const model_t *model) tess_static_plain : tess_lerped_plain; } - GL_RotateForEntity(); + GL_RotateForEntity(false); GL_BindArrays(dotshading ? VA_MESH_SHADE : VA_MESH_FLAT); diff --git a/src/refresh/models.c b/src/refresh/models.c index ed74864a7..8976fdd34 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -213,7 +213,6 @@ static int MOD_LoadSP2(model_t *model, const void *rawdata, size_t length) Com_WPrintf("%s has bad frame name\n", model->name); dst_frame->image = R_NOTEXTURE; } else { - FS_NormalizePath(buffer); dst_frame->image = IMG_Find(buffer, IT_SPRITE, IF_NONE); } @@ -395,7 +394,6 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) ret = Q_ERR_STRING_TRUNCATED; goto fail; } - FS_NormalizePath(skinname); mesh->skins[i] = IMG_Find(skinname, IT_SKIN, IF_NONE); src_skin += MD2_MAX_SKINNAME; } @@ -546,7 +544,6 @@ static int MOD_LoadMD3Mesh(model_t *model, maliasmesh_t *mesh, #endif if (!Q_memccpy(skinname, src_skin->name, 0, sizeof(maliasskinname_t))) return Q_ERR_STRING_TRUNCATED; - FS_NormalizePath(skinname); mesh->skins[i] = IMG_Find(skinname, IT_SKIN, IF_NONE); src_skin++; } diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 24a8169ad..e216d97a1 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -43,6 +43,7 @@ static void write_block(sizebuf_t *buf) GLSF("layout(std140) uniform u_block {\n"); GLSL( mat4 m_vp; + mat4 m_sky[2]; float u_time; float u_modulate; float u_add; @@ -52,8 +53,6 @@ static void write_block(sizebuf_t *buf) vec2 w_amp; vec2 w_phase; vec2 u_scroll; - vec3 u_vieworg; - float pad_2; ) GLSF("};\n"); } @@ -64,7 +63,7 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) write_block(buf); GLSL(in vec4 a_pos;) - if (bits & GLS_CLASSIC_SKY) { + if (bits & GLS_SKY_MASK) { GLSL(out vec3 v_dir;) } else { GLSL(in vec2 a_tc;) @@ -83,8 +82,9 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) GLSF("void main() {\n"); if (bits & GLS_CLASSIC_SKY) { - GLSL(v_dir = a_pos.xyz - u_vieworg.xyz;) - GLSL(v_dir[2] *= 3.0;) + GLSL(v_dir = (m_sky[1] * a_pos).xyz;) + } else if (bits & GLS_DEFAULT_SKY) { + GLSL(v_dir = (m_sky[0] * a_pos).xyz;) } else if (bits & GLS_SCROLL_ENABLE) { GLSL(v_tc = a_tc + u_scroll;) } else { @@ -108,20 +108,25 @@ static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) if (gl_config.ver_es) GLSL(precision mediump float;) - if (bits & (GLS_WARP_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_INTENSITY_ENABLE | GLS_CLASSIC_SKY)) + if (bits & (GLS_WARP_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_INTENSITY_ENABLE | GLS_SKY_MASK)) write_block(buf); if (bits & GLS_CLASSIC_SKY) { GLSL( uniform sampler2D u_texture1; uniform sampler2D u_texture2; - in vec3 v_dir; ) + } else if (bits & GLS_DEFAULT_SKY) { + GLSL(uniform samplerCube u_texture;) } else { GLSL(uniform sampler2D u_texture;) - GLSL(in vec2 v_tc;) } + if (bits & GLS_SKY_MASK) + GLSL(in vec3 v_dir;) + else + GLSL(in vec2 v_tc;) + if (bits & GLS_LIGHTMAP_ENABLE) { GLSL(uniform sampler2D u_lightmap;) GLSL(in vec2 v_lmtc;) @@ -146,6 +151,8 @@ static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) vec4 alpha = texture(u_texture2, tc2); vec4 diffuse = vec4((solid.rgb - alpha.rgb * 0.25) * 0.65, 1.0); ) + } else if (bits & GLS_DEFAULT_SKY) { + GLSL(vec4 diffuse = texture(u_texture, v_dir);) } else { GLSL(vec2 tc = v_tc;) @@ -255,7 +262,7 @@ static GLuint create_and_use_program(glStateBits_t bits) qglAttachShader(program, shader_f); qglBindAttribLocation(program, VERT_ATTR_POS, "a_pos"); - if (!(bits & GLS_CLASSIC_SKY)) + if (!(bits & GLS_SKY_MASK)) qglBindAttribLocation(program, VERT_ATTR_TC, "a_tc"); if (bits & GLS_LIGHTMAP_ENABLE) qglBindAttribLocation(program, VERT_ATTR_LMTC, "a_lmtc"); @@ -413,8 +420,6 @@ static void shader_setup_2d(void) gls.u_block.w_amp[1] = 0.0025f; gls.u_block.w_phase[0] = M_PIf * 10; gls.u_block.w_phase[1] = M_PIf * 10; - - VectorClear(gls.u_block.vieworg); } static void shader_setup_3d(void) @@ -430,7 +435,9 @@ static void shader_setup_3d(void) gls.u_block.w_phase[0] = 4; gls.u_block.w_phase[1] = 4; - VectorCopy(glr.fd.vieworg, gls.u_block.vieworg); + R_RotateForSky(); + + memcpy(gls.u_block.msky, glr.skymatrix, sizeof(glr.skymatrix)); } static void shader_disable_state(void) @@ -444,6 +451,8 @@ static void shader_disable_state(void) qglActiveTexture(GL_TEXTURE0); qglBindTexture(GL_TEXTURE_2D, 0); + qglBindTexture(GL_TEXTURE_CUBE_MAP, 0); + for (int i = 0; i < VERT_ATTR_COUNT; i++) qglDisableVertexAttribArray(i); } @@ -462,6 +471,9 @@ static void shader_init(void) qglBindBuffer(GL_UNIFORM_BUFFER, gl_static.uniform_buffer); qglBindBufferBase(GL_UNIFORM_BUFFER, 0, gl_static.uniform_buffer); qglBufferData(GL_UNIFORM_BUFFER, sizeof(gls.u_block), NULL, GL_DYNAMIC_DRAW); + + if (gl_config.ver_gl >= QGL_VER(3, 2)) + qglEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); } static void shader_shutdown(void) @@ -483,6 +495,9 @@ static void shader_shutdown(void) qglDeleteBuffers(1, &gl_static.uniform_buffer); gl_static.uniform_buffer = 0; } + + if (gl_config.ver_gl >= QGL_VER(3, 2)) + qglDisable(GL_TEXTURE_CUBE_MAP_SEAMLESS); } const glbackend_t backend_shader = { diff --git a/src/refresh/sky.c b/src/refresh/sky.c index f4afe21e3..ef54248ee 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -334,24 +334,15 @@ void R_DrawSkyBox(void) return; // nothing visible GL_BindArrays(VA_SPRITE); - - if (gl_static.classic_sky && gl_drawsky->integer == 1) { - GL_StateBits(GLS_TEXTURE_REPLACE | GLS_CLASSIC_SKY); - GL_ArrayBits(GLA_VERTEX); - GL_BindTexture(TMU_TEXTURE, gl_static.classic_sky->texnum); - GL_BindTexture(TMU_LIGHTMAP, gl_static.classic_sky->texnum2); - } else { - GL_StateBits(GLS_TEXTURE_REPLACE); - GL_ArrayBits(GLA_VERTEX | GLA_TC); - } + GL_StateBits(GLS_TEXTURE_REPLACE); + GL_ArrayBits(GLA_VERTEX | GLA_TC); for (i = 0; i < 6; i++) { if (skymins[0][i] >= skymaxs[0][i] || skymins[1][i] >= skymaxs[1][i]) continue; - if (!gl_static.classic_sky || gl_drawsky->integer != 1) - GL_BindTexture(TMU_TEXTURE, sky_images[i]); + GL_BindTexture(TMU_TEXTURE, sky_images[i]); MakeSkyVec(skymaxs[0][i], skymins[1][i], i, tess.vertices); MakeSkyVec(skymins[0][i], skymins[1][i], i, tess.vertices + 5); @@ -364,13 +355,82 @@ void R_DrawSkyBox(void) } } +static void DefaultSkyMatrix(GLfloat *matrix) +{ + if (skyautorotate) { + SetupRotationMatrix(skymatrix, skyaxis, glr.fd.time * skyrotate); + TransposeAxis(skymatrix); + } + + matrix[ 0] = skymatrix[0][0]; + matrix[ 4] = skymatrix[0][1]; + matrix[ 8] = skymatrix[0][2]; + matrix[12] = -DotProduct(skymatrix[0], glr.fd.vieworg); + + matrix[ 1] = skymatrix[2][0]; + matrix[ 5] = skymatrix[2][1]; + matrix[ 9] = skymatrix[2][2]; + matrix[13] = -DotProduct(skymatrix[2], glr.fd.vieworg); + + matrix[ 2] = skymatrix[1][0]; + matrix[ 6] = skymatrix[1][1]; + matrix[10] = skymatrix[1][2]; + matrix[14] = -DotProduct(skymatrix[1], glr.fd.vieworg); + + matrix[ 3] = 0; + matrix[ 7] = 0; + matrix[11] = 0; + matrix[15] = 1; +} + +// classic skies don't rotate +static void ClassicSkyMatrix(GLfloat *matrix) +{ + matrix[ 0] = 1; + matrix[ 4] = 0; + matrix[ 8] = 0; + matrix[12] = -glr.fd.vieworg[0]; + + matrix[ 1] = 0; + matrix[ 5] = 1; + matrix[ 9] = 0; + matrix[13] = -glr.fd.vieworg[1]; + + matrix[ 2] = 0; + matrix[ 6] = 0; + matrix[10] = 3; + matrix[14] = -glr.fd.vieworg[2] * 3; + + matrix[ 3] = 0; + matrix[ 7] = 0; + matrix[11] = 0; + matrix[15] = 1; +} + +/* +============ +R_RotateForSky +============ +*/ +void R_RotateForSky(void) +{ + if (!gl_static.use_cubemaps) + return; + + DefaultSkyMatrix(glr.skymatrix[0]); + ClassicSkyMatrix(glr.skymatrix[1]); +} + static void R_UnsetSky(void) { int i; skyrotate = 0; + skyautorotate = false; for (i = 0; i < 6; i++) sky_images[i] = TEXNUM_BLACK; + + R_SKYTEXTURE->texnum = TEXNUM_CUBEMAP_BLACK; } /* @@ -383,8 +443,9 @@ void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis int i; char pathname[MAX_QPATH]; const image_t *image; + imageflags_t flags = IF_NONE; - if (!gl_drawsky->integer || (gl_static.classic_sky && gl_drawsky->integer == 1)) { + if (!gl_drawsky->integer) { R_UnsetSky(); return; } @@ -392,18 +453,36 @@ void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis skyrotate = rotate; skyautorotate = autorotate; VectorNormalize2(axis, skyaxis); - if (!skyautorotate) - SetupRotationMatrix(skymatrix, skyaxis, skyrotate); + SetupRotationMatrix(skymatrix, skyaxis, skyrotate); + if (gl_static.use_cubemaps) + TransposeAxis(skymatrix); + if (!skyrotate) + skyautorotate = false; + + // try to load cubemap image first + if (gl_static.use_cubemaps) { + if (Q_concat(pathname, sizeof(pathname), "sky/", name, ".tga") >= sizeof(pathname)) { + R_UnsetSky(); + return; + } + image = IMG_Find(pathname, IT_SKY, IF_CUBEMAP); + if (image != R_SKYTEXTURE) { + R_SKYTEXTURE->texnum = image->texnum; + return; + } + R_SKYTEXTURE->texnum = TEXNUM_CUBEMAP_DEFAULT; + flags = IF_CUBEMAP | IF_TURBULENT; // hack for IMG_Load() + } + // load legacy skybox for (i = 0; i < 6; i++) { if (Q_concat(pathname, sizeof(pathname), "env/", name, com_env_suf[i], ".tga") >= sizeof(pathname)) { R_UnsetSky(); return; } - FS_NormalizePath(pathname); - image = IMG_Find(pathname, IT_SKY, IF_NONE); - if (image == R_NOTEXTURE) { + image = IMG_Find(pathname, IT_SKY, flags); + if (image == R_SKYTEXTURE) { R_UnsetSky(); return; } diff --git a/src/refresh/state.c b/src/refresh/state.c index ebfde37fd..2684477a2 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -60,6 +60,38 @@ void GL_BindTexture(glTmu_t tmu, GLuint texnum) c.texSwitches++; } +void GL_ForceCubemap(GLuint texnum) +{ + GL_ActiveTexture(TMU_TEXTURE); + + if (gls.texnumcube == texnum) + return; + + qglBindTexture(GL_TEXTURE_CUBE_MAP, texnum); + gls.texnumcube = texnum; + + c.texSwitches++; +} + +void GL_BindCubemap(GLuint texnum) +{ + if (!gl_drawsky->integer) + texnum = TEXNUM_CUBEMAP_BLACK; + + if (gls.texnumcube == texnum) + return; + + if (qglBindTextureUnit) { + qglBindTextureUnit(TMU_TEXTURE, texnum); + } else { + GL_ActiveTexture(TMU_TEXTURE); + qglBindTexture(GL_TEXTURE_CUBE_MAP, texnum); + } + gls.texnumcube = texnum; + + c.texSwitches++; +} + void GL_CommonStateBits(glStateBits_t bits) { glStateBits_t diff = bits ^ gls.state_bits; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index df3d0b026..169a074aa 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -573,6 +573,13 @@ static glStateBits_t statebits_for_surface(const mface_t *surf) { glStateBits_t statebits = GLS_DEFAULT; + if (surf->drawflags & SURF_SKY) { + if (Q_stricmpn(surf->texinfo->name, CONST_STR_LEN("n64/env/sky")) == 0) + return GLS_TEXTURE_REPLACE | GLS_CLASSIC_SKY; + else + return GLS_TEXTURE_REPLACE | GLS_DEFAULT_SKY; + } + if (gl_static.use_shaders) { // no inverse intensity if (!(surf->drawflags & SURF_TRANS_MASK)) @@ -887,7 +894,9 @@ static void upload_world_surfaces(void) currvert = 0; lastvert = 0; for (i = 0, surf = bsp->faces; i < bsp->numfaces; i++, surf++) { - if (surf->drawflags & (SURF_SKY | SURF_NODRAW)) + if (surf->drawflags & SURF_SKY && !gl_static.use_cubemaps) + continue; + if (surf->drawflags & SURF_NODRAW) continue; Q_assert(surf->numsurfedges >= 3 && surf->numsurfedges <= TESS_MAX_VERTICES); @@ -974,7 +983,6 @@ void GL_FreeWorld(void) return; BSP_Free(gl_static.world.cache); - gl_static.classic_sky = NULL; if (gl_static.world.vertices) Z_Free(gl_static.world.vertices); @@ -1032,28 +1040,27 @@ void GL_LoadWorld(const char *name) GL_InitQueries(); gl_static.world.cache = bsp; - gl_static.classic_sky = NULL; // calculate world size for far clip plane and sky box set_world_size(bsp->nodes); // register all texinfo for (i = 0, info = bsp->texinfo; i < bsp->numtexinfo; i++, info++) { - if (gl_static.use_shaders && info->c.flags & SURF_SKY && - Q_stricmpn(info->name, CONST_STR_LEN("n64/env/sky")) == 0) { - if (gl_static.classic_sky) { - info->image = gl_static.classic_sky; - } else { + if (info->c.flags & SURF_SKY) { + if (!gl_static.use_cubemaps) { + info->image = R_NOTEXTURE; + } else if (Q_stricmpn(info->name, CONST_STR_LEN("n64/env/sky")) == 0) { Q_concat(buffer, sizeof(buffer), "textures/", info->name, ".tga"); - FS_NormalizePath(buffer); info->image = IMG_Find(buffer, IT_SKY, IF_REPEAT | IF_CLASSIC_SKY); - if (info->image != R_NOTEXTURE) - gl_static.classic_sky = info->image; + } else if (Q_stricmpn(info->name, CONST_STR_LEN("sky/")) == 0) { + Q_concat(buffer, sizeof(buffer), info->name, ".tga"); + info->image = IMG_Find(buffer, IT_SKY, IF_CUBEMAP); + } else { + info->image = R_SKYTEXTURE; } } else { imageflags_t flags = (info->c.flags & SURF_WARP) ? IF_TURBULENT : IF_NONE; Q_concat(buffer, sizeof(buffer), "textures/", info->name, ".wal"); - FS_NormalizePath(buffer); info->image = IMG_Find(buffer, IT_WALL, flags); } } @@ -1064,8 +1071,16 @@ void GL_LoadWorld(const char *name) // hack surface flags into drawflags for faster access surf->drawflags |= surf->texinfo->c.flags & ~DSURF_PLANEBACK; + // clear statebits from previous load + surf->statebits = GLS_DEFAULT; + // don't count sky surfaces - if (surf->drawflags & (SURF_SKY | SURF_NODRAW)) + if (surf->drawflags & SURF_SKY) { + if (!gl_static.use_cubemaps) + continue; + surf->drawflags &= ~SURF_NODRAW; + } + if (surf->drawflags & SURF_NODRAW) continue; if (surf->drawflags & SURF_N64_UV) n64surfs++; @@ -1082,12 +1097,15 @@ void GL_LoadWorld(const char *name) } gl_static.nolm_mask = SURF_NOLM_MASK_DEFAULT; + gl_static.use_bmodel_skies = false; // only supported in DECOUPLED_LM maps because vanilla maps have broken // lightofs for liquids/alphas. legacy renderer doesn't support lightmapped // liquids too. - if ((bsp->lm_decoupled || n64surfs > 100 || gl_static.classic_sky) && gl_static.use_shaders) + if ((bsp->lm_decoupled || n64surfs > 100) && gl_static.use_shaders) { gl_static.nolm_mask = SURF_NOLM_MASK_REMASTER; + gl_static.use_bmodel_skies = gl_static.use_cubemaps; + } glr.fd.lightstyles = &(lightstyle_t){ 1 }; diff --git a/src/refresh/tess.c b/src/refresh/tess.c index d7d4e080d..ce8450ceb 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -638,7 +638,9 @@ void GL_Flush3D(void) if (!tess.numindices) return; - if (q_likely(tess.texnum[TMU_LIGHTMAP])) { + if (q_unlikely(state & GLS_SKY_MASK)) { + array = GLA_VERTEX; + } else if (q_likely(tess.texnum[TMU_LIGHTMAP])) { state |= GLS_LIGHTMAP_ENABLE; array |= GLA_LMTC; @@ -655,7 +657,9 @@ void GL_Flush3D(void) GL_StateBits(state); GL_ArrayBits(array); - if (qglBindTextures) { + if (state & GLS_DEFAULT_SKY) { + GL_BindCubemap(tess.texnum[0]); + } else if (qglBindTextures) { #if USE_DEBUG if (q_unlikely(gl_nobind->integer)) tess.texnum[TMU_TEXTURE] = TEXNUM_DEFAULT; @@ -715,8 +719,9 @@ static const image_t *GL_TextureAnimation(const mtexinfo_t *tex) static void GL_DrawFace(const mface_t *surf) { const image_t *image = GL_TextureAnimation(surf->texinfo); - int numtris = surf->numsurfedges - 2; - int numindices = numtris * 3; + const int numtris = surf->numsurfedges - 2; + const int numindices = numtris * 3; + glStateBits_t state = surf->statebits; GLuint texnum[MAX_TMUS] = { 0 }; glIndex_t *dst_indices; int i, j; @@ -730,10 +735,17 @@ static void GL_DrawFace(const mface_t *surf) texnum[TMU_TEXTURE] = TEXNUM_WHITE; texnum[TMU_GLOWMAP] = 0; } + } else if (state & GLS_CLASSIC_SKY) { + if (q_likely(gl_drawsky->integer)) { + texnum[TMU_LIGHTMAP] = image->texnum2; + } else { + texnum[TMU_TEXTURE ] = TEXNUM_BLACK; + state &= ~GLS_CLASSIC_SKY; + } } if (memcmp(tess.texnum, texnum, sizeof(texnum)) || - tess.flags != surf->statebits || + tess.flags != state || tess.numindices + numindices > TESS_MAX_INDICES) GL_Flush3D(); @@ -752,7 +764,7 @@ static void GL_DrawFace(const mface_t *surf) tess.numindices += numindices; memcpy(tess.texnum, texnum, sizeof(texnum)); - tess.flags = surf->statebits; + tess.flags = state; c.facesTris += numtris; c.facesDrawn++; @@ -787,7 +799,9 @@ void GL_DrawAlphaFaces(void) glr.ent = face->entity; GL_Flush3D(); GL_SetEntityAxis(); - GL_RotateForEntity(); + GL_RotateForEntity(glr.ent == &gl_world ? + gl_static.use_cubemaps : + gl_static.use_bmodel_skies); } GL_DrawFace(face); } diff --git a/src/refresh/texture.c b/src/refresh/texture.c index eec063a01..4a5e501dd 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -29,6 +29,7 @@ static int gl_tex_solid_format; static int upload_width; static int upload_height; static bool upload_alpha; +static GLenum upload_target; static cvar_t *gl_noscrap; static cvar_t *gl_round_down; @@ -45,14 +46,15 @@ static cvar_t *gl_saturation; static cvar_t *gl_gamma; static cvar_t *gl_invert; static cvar_t *gl_partshape; +static cvar_t *gl_cubemaps; cvar_t *gl_intensity; -image_t shell_texture; static int GL_UpscaleLevel(int width, int height, imagetype_t type, imageflags_t flags); static void GL_Upload32(byte *data, int width, int height, int baselevel, imagetype_t type, imageflags_t flags); static void GL_Upscale32(byte *data, int width, int height, int maxlevel, imagetype_t type, imageflags_t flags); static void GL_SetFilterAndRepeat(imagetype_t type, imageflags_t flags); +static void GL_SetCubemapFilterAndRepeat(void); static void GL_InitRawTexture(void); typedef struct { @@ -82,13 +84,20 @@ static void update_image_params(unsigned mask) continue; if (!(mask & BIT(image->type))) continue; + if (!image->texnum) + continue; - GL_ForceTexture(TMU_TEXTURE, image->texnum); - GL_SetFilterAndRepeat(image->type, image->flags); - - if (image->texnum2) { - GL_ForceTexture(TMU_TEXTURE, image->texnum2); + if (image->flags & IF_CUBEMAP) { + GL_ForceCubemap(image->texnum); + GL_SetCubemapFilterAndRepeat(); + } else { + GL_ForceTexture(TMU_TEXTURE, image->texnum); GL_SetFilterAndRepeat(image->type, image->flags); + + if (image->texnum2) { + GL_ForceTexture(TMU_TEXTURE, image->texnum2); + GL_SetFilterAndRepeat(image->type, image->flags); + } } } } @@ -506,8 +515,12 @@ static void GL_Upload32(byte *data, int width, int height, int baselevel, imaget if (upload_alpha) comp = gl_tex_alpha_format; - qglTexImage2D(GL_TEXTURE_2D, baselevel, comp, scaled_width, - scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaled); + if (flags & IF_CUBEMAP) + qglTexImage2D(upload_target, baselevel, GL_RGBA, scaled_width, + scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaled); + else + qglTexImage2D(GL_TEXTURE_2D, baselevel, comp, scaled_width, + scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaled); c.texUploads++; @@ -660,6 +673,141 @@ static void GL_SetFilterAndRepeat(imagetype_t type, imageflags_t flags) } } +static const char gl_env_suf[6][2] = { + "rt", "lf", "up", "dn", "bk", "ft" +}; + +// format: 2 byte aspect ratio, then 6 pairs of (s, t) offsets +static const byte gl_env_ofs[6][14] = { + { 4, 3, 2, 1, 0, 1, 1, 0, 1, 2, 1, 1, 3, 1 }, // horizontal cross + { 3, 4, 2, 1, 0, 1, 1, 0, 1, 2, 1, 1, 1, 3 }, // vertical cross + { 6, 1, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0 }, // single row + { 1, 6, 0, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5 }, // single column + { 3, 2, 0, 0, 0, 1, 1, 0, 1, 1, 2, 0, 2, 1 }, // double row + { 2, 3, 0, 0, 1, 0, 0, 1, 1, 1, 0, 2, 1, 2 }, // double column +}; + +static void GL_SetCubemapFilterAndRepeat(void) +{ + qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, gl_filter_max); + qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, gl_filter_max); + + qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); +} + +#define PIX(x, y) pic[(y) * n + (x)] + +static void GL_RotateImageCW(uint32_t *pic, int n) +{ + for (int i = 0; i < n / 2; i++) { + for (int j = i; j < n - i - 1; j++) { + uint32_t temp = PIX(i, j); + PIX(i, j) = PIX(n - j - 1, i); + PIX(n - j - 1, i) = PIX(n - i - 1, n - j - 1); + PIX(n - i - 1, n - j - 1) = PIX(j, n - i - 1); + PIX(j, n - i - 1) = temp; + } + } +} + +static void GL_RotateImageCCW(uint32_t *pic, int n) +{ + for (int i = 0; i < n / 2; i++) { + for (int j = i; j < n - i - 1; j++) { + uint32_t temp = PIX(i, j); + PIX(i, j) = PIX(j, n - i - 1); + PIX(j, n - i - 1) = PIX(n - i - 1, n - j - 1); + PIX(n - i - 1, n - j - 1) = PIX(n - j - 1, i); + PIX(n - j - 1, i) = temp; + } + } +} + +#undef PIX + +// upload one side of legacy skybox +static bool GL_UploadSkyboxSide(image_t *image, byte *pic) +{ + int width = image->upload_width; + int height = image->upload_height; + int i; + + // it should be safe to assume non-cube skyboxes don't exist, + // so don't bother with resampling. + if (width != height) + return false; + + Q_assert(image->baselen >= 2); + const char *s = image->name + image->baselen - 2; + + for (i = 0; i < 6; i++) + if (!memcmp(s, gl_env_suf[i], 2)) + break; + Q_assert(i < 6); + upload_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i; + + if (i == 2) + GL_RotateImageCW((uint32_t *)pic, width); + else if (i == 3) + GL_RotateImageCCW((uint32_t *)pic, width); + + GL_ForceCubemap(TEXNUM_CUBEMAP_DEFAULT); + GL_Upload32(pic, width, height, 0, image->type, image->flags); + + image->upload_width = upload_width; + image->upload_height = upload_height; + return true; +} + +static bool GL_UploadCubemap(image_t *image, byte *pic) +{ + int width = image->upload_width; + int height = image->upload_height; + const byte *ofs, *src; + byte *buffer, *dst; + int i, s, t, size; + + if (image->flags & IF_TURBULENT) + return GL_UploadSkyboxSide(image, pic); + + // figure out layout from aspect ratio + for (i = 0; i < q_countof(gl_env_ofs); i++) { + ofs = gl_env_ofs[i]; + if (width * ofs[1] == height * ofs[0]) + break; + } + if (i == q_countof(gl_env_ofs)) + return false; + + qglGenTextures(1, &image->texnum); + GL_ForceCubemap(image->texnum); + GL_SetCubemapFilterAndRepeat(); + + // need to make a copy here because of resampling + size = width / ofs[0]; + buffer = FS_AllocTempMem(size * size * 4); + + ofs += 2; + for (i = 0; i < 6; i++, ofs += 2) { + s = ofs[0] * size; + t = ofs[1] * size; + src = pic + ((t * width) + s) * 4; + dst = buffer; + for (int j = 0; j < size; j++) { + memcpy(dst, src, size * 4); + src += width * 4; + dst += size * 4; + } + upload_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i; + GL_Upload32(buffer, size, size, 0, image->type, image->flags); + } + + FS_FreeTempMem(buffer); + return true; +} + /* ================ IMG_Load @@ -671,6 +819,13 @@ void IMG_Load(image_t *image, byte *pic) int i, s, t, maxlevel; int width, height; + if (image->flags & IF_CUBEMAP) { + if (GL_UploadCubemap(image, pic)) + return; + Com_WPrintf("%s has bad aspect ratio for cubemap\n", image->name); + image->flags &= ~IF_CUBEMAP; + } + width = image->upload_width; height = image->upload_height; @@ -732,6 +887,9 @@ void IMG_Unload(image_t *image) if (gls.texnums[i] == tex[0] || gls.texnums[i] == tex[1]) gls.texnums[i] = 0; + if (gls.texnumcube == tex[0]) + gls.texnumcube = 0; + qglDeleteTextures(tex[1] ? 2 : 1, tex); image->texnum = image->texnum2 = 0; } @@ -929,6 +1087,9 @@ static void GL_InitWhiteImage(void) GL_ForceTexture(TMU_TEXTURE, TEXNUM_BLACK); GL_Upload32((byte *)&pixel, 1, 1, 0, IT_SPRITE, IF_REPEAT | IF_NEAREST); GL_SetFilterAndRepeat(IT_SPRITE, IF_REPEAT | IF_NEAREST); + + // init shell texture (don't set name to keep it immutable) + R_SHELLTEXTURE->texnum = TEXNUM_WHITE; } static void GL_InitBeamTexture(void) @@ -962,6 +1123,28 @@ static void GL_InitRawTexture(void) GL_SetFilterAndRepeat(IT_PIC, IF_NONE); } +static void GL_InitCubemaps(void) +{ + gl_static.use_cubemaps = gl_static.use_shaders && gl_cubemaps->integer; + if (!gl_static.use_cubemaps) + return; + + // default cubemap for legacy skybox + GL_ForceCubemap(TEXNUM_CUBEMAP_DEFAULT); + GL_SetCubemapFilterAndRepeat(); + + // intentionally incomplete cubemap that will always return black + GL_ForceCubemap(TEXNUM_CUBEMAP_BLACK); + GL_SetCubemapFilterAndRepeat(); + + // init default sky texture + image_t *sky = R_SKYTEXTURE; + strcpy(sky->name, "SKYTEXTURE"); + sky->type = IT_SKY; + sky->flags = IF_CUBEMAP; + sky->texnum = TEXNUM_CUBEMAP_DEFAULT; +} + bool GL_InitWarpTexture(void) { GL_ClearErrors(); @@ -1046,6 +1229,7 @@ void GL_InitImages(void) gl_gamma = Cvar_Get("vid_gamma", "1", CVAR_ARCHIVE); gl_partshape = Cvar_Get("gl_partshape", "0", 0); gl_partshape->changed = gl_partshape_changed; + gl_cubemaps = Cvar_Get("gl_cubemaps", "1", CVAR_FILES); if (r_config.flags & QVF_GAMMARAMP) { gl_gamma->changed = gl_gamma_changed; @@ -1083,7 +1267,6 @@ void GL_InitImages(void) qglGenTextures(NUM_AUTO_TEXTURES, gl_static.texnums); qglGenTextures(LM_MAX_LIGHTMAPS, lm.texnums); - shell_texture.texnum = TEXNUM_WHITE; if (gl_static.use_shaders) { qglGenTextures(1, &gl_static.warp_texture); @@ -1098,6 +1281,7 @@ void GL_InitImages(void) GL_InitWhiteImage(); GL_InitBeamTexture(); GL_InitRawTexture(); + GL_InitCubemaps(); #if USE_DEBUG r_charset = R_RegisterFont("conchars"); @@ -1127,7 +1311,6 @@ void GL_ShutdownImages(void) memset(gl_static.texnums, 0, sizeof(gl_static.texnums)); memset(lm.texnums, 0, sizeof(lm.texnums)); - memset(&shell_texture, 0, sizeof(shell_texture)); GL_DeleteWarpTexture(); diff --git a/src/refresh/world.c b/src/refresh/world.c index efc3a0868..b204c9816 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -403,6 +403,7 @@ void GL_DrawBspModel(mmodel_t *model) vec3_t transformed, temp; entity_t *ent = glr.ent; glCullResult_t cull; + glStateBits_t skymask; int i; if (!model->numfaces) @@ -440,7 +441,9 @@ void GL_DrawBspModel(mmodel_t *model) GL_TransformLights(model); - GL_RotateForEntity(); + GL_RotateForEntity(gl_static.use_bmodel_skies); + + skymask = gl_static.use_bmodel_skies ? GLS_SKY_MASK : 0; GL_BindArrays(VA_3D); @@ -449,7 +452,9 @@ void GL_DrawBspModel(mmodel_t *model) // draw visible faces for (i = 0, face = model->firstface; i < model->numfaces; i++, face++) { // sky faces don't have their polygon built - if (face->drawflags & (SURF_SKY | SURF_NODRAW)) + if (face->drawflags & SURF_SKY && !(face->statebits & skymask)) + continue; + if (face->drawflags & SURF_NODRAW) continue; dot = PlaneDiffFast(transformed, face->plane); @@ -531,7 +536,7 @@ static inline void GL_DrawNode(const mnode_t *node) if (face->drawframe != glr.drawframe) continue; - if (face->drawflags & SURF_SKY) { + if (face->drawflags & SURF_SKY && !(face->statebits & GLS_SKY_MASK)) { R_AddSkySurface(face); continue; } From 2b3f6f603fbb7db29cdebf0df49a4baabfeb8e9d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 15 Sep 2024 11:19:51 +0300 Subject: [PATCH 03/43] Rename IF_DIRECT flag. --- inc/refresh/refresh.h | 5 +++-- src/common/tests.c | 2 +- src/refresh/images.c | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index d2c6617c9..d3196ea4b 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -157,9 +157,10 @@ typedef enum { IF_DEFAULT_FLARE = BIT(9), // default flare hack IF_CUBEMAP = BIT(10), // cubemap (or part of it) - // not stored in image + // these flags only affect R_RegisterImage() behavior, + // and are not stored in image IF_OPTIONAL = BIT(16), // don't warn if not found - IF_DIRECT = BIT(17), // don't override extension + IF_KEEP_EXTENSION = BIT(17), // don't override extension IF_CLASSIC_SKY = BIT(18), // split in two halves } imageflags_t; diff --git a/src/common/tests.c b/src/common/tests.c index ae714bcdb..baf1a5d93 100644 --- a/src/common/tests.c +++ b/src/common/tests.c @@ -599,7 +599,7 @@ static void Com_TestImages_f(void) R_EndRegistration(); R_BeginRegistration(NULL); } - if (!R_RegisterImage(va("/%s", (char *)list[i]), IT_PIC, IF_DIRECT)) { + if (!R_RegisterImage(va("/%s", (char *)list[i]), IT_PIC, IF_KEEP_EXTENSION)) { errors++; continue; } diff --git a/src/refresh/images.c b/src/refresh/images.c index e6d6f83e7..86849fe31 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1964,7 +1964,7 @@ static image_t *find_or_load_image(const char *name, size_t len, // load the pic from disk pic = NULL; - if (flags & IF_DIRECT) { + if (flags & IF_KEEP_EXTENSION) { // direct load requested (for testing code) if (fmt == IM_MAX) ret = Q_ERR_INVALID_PATH; From ee0ec72a7d96e907f05973bc8e2ddb0e42c2bfb9 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 15 Sep 2024 11:19:52 +0300 Subject: [PATCH 04/43] Bump MAX_TEXTURE_SIZE. --- src/refresh/images.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/images.h b/src/refresh/images.h index c1646b5da..cd2d2067f 100644 --- a/src/refresh/images.h +++ b/src/refresh/images.h @@ -38,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define U32_RGB MakeColor(255, 255, 255, 0) // absolute limit for OpenGL renderer -#define MAX_TEXTURE_SIZE 4096 +#define MAX_TEXTURE_SIZE 8192 typedef enum { IM_PCX, From 34bd2f11e3fe2d5f93c948d6b9804276355f7044 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 15 Sep 2024 11:19:52 +0300 Subject: [PATCH 05/43] =?UTF-8?q?Add=20=E2=80=98cl=5Fsurface=E2=80=99=20ma?= =?UTF-8?q?cro.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/main.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/client/main.c b/src/client/main.c index 6a4aadc34..cf1946658 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -2283,6 +2283,19 @@ static size_t CL_NumEntities_m(char *buffer, size_t size) return Q_snprintf(buffer, size, "%i", cl.frame.numEntities); } +static size_t CL_Surface_m(char *buffer, size_t size) +{ + trace_t trace; + vec3_t end; + + if (cls.state != ca_active) + return Q_strlcpy(buffer, "", size); + + VectorMA(cl.refdef.vieworg, 8192, cl.v_forward, end); + CL_Trace(&trace, cl.refdef.vieworg, end, vec3_origin, vec3_origin, MASK_SOLID | MASK_WATER); + return Q_strlcpy(buffer, trace.surface->name, size); +} + /* =============== CL_WriteConfig @@ -2812,6 +2825,7 @@ static void CL_InitLocal(void) Cmd_AddMacro("cl_armor", CL_Armor_m); Cmd_AddMacro("cl_weaponmodel", CL_WeaponModel_m); Cmd_AddMacro("cl_numentities", CL_NumEntities_m); + Cmd_AddMacro("cl_surface", CL_Surface_m); } static const cmdreg_t c_ignores[] = { From 4bbcb4ab6c57646a85c389cfd1e2101abb1171fa Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 15 Sep 2024 11:19:52 +0300 Subject: [PATCH 06/43] Remove redundant argument. --- src/refresh/gl.h | 2 +- src/refresh/tess.c | 4 ++-- src/refresh/world.c | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index e302eb6b2..4230ac14c 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -772,7 +772,7 @@ void GL_ShutdownArrays(void); void GL_Flush3D(void); -void GL_AddAlphaFace(mface_t *face, entity_t *ent); +void GL_AddAlphaFace(mface_t *face); void GL_AddSolidFace(mface_t *face); void GL_DrawAlphaFaces(void); void GL_DrawSolidFaces(void); diff --git a/src/refresh/tess.c b/src/refresh/tess.c index ce8450ceb..dd6380d37 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -819,10 +819,10 @@ void GL_AddSolidFace(mface_t *face) faces_next[face->hash] = &face->next; } -void GL_AddAlphaFace(mface_t *face, entity_t *ent) +void GL_AddAlphaFace(mface_t *face) { // draw back-to-front - face->entity = ent; + face->entity = glr.ent; face->next = faces_alpha; faces_alpha = face; } diff --git a/src/refresh/world.c b/src/refresh/world.c index b204c9816..682d39314 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -468,7 +468,7 @@ void GL_DrawBspModel(mmodel_t *model) if (face->drawflags & SURF_TRANS_MASK) { if (model->drawframe != glr.drawframe) - GL_AddAlphaFace(face, ent); + GL_AddAlphaFace(face); continue; } @@ -548,7 +548,7 @@ static inline void GL_DrawNode(const mnode_t *node) GL_PushLights(face); if (face->drawflags & SURF_TRANS_MASK) - GL_AddAlphaFace(face, &gl_world); + GL_AddAlphaFace(face); else GL_AddSolidFace(face); } From d06914c81ef27539c40867ad579589c3623c20f8 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 16 Sep 2024 01:48:28 +0300 Subject: [PATCH 07/43] Assert world vertex buffer doesn't overflow. --- src/refresh/gl.h | 1 + src/refresh/surf.c | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 4230ac14c..b0b4a8df3 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -69,6 +69,7 @@ typedef struct { bsp_t *cache; vec_t *vertices; GLuint buffer; + size_t buffer_size; vec_t size; } world; GLuint warp_texture; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 169a074aa..ba0f25e8d 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -878,6 +878,7 @@ static void check_multitexture(void) static void upload_world_surfaces(void) { const bsp_t *bsp = gl_static.world.cache; + size_t size = gl_static.world.buffer_size; vec_t *vbo; mface_t *surf; int i, currvert, lastvert; @@ -900,6 +901,8 @@ static void upload_world_surfaces(void) continue; Q_assert(surf->numsurfedges >= 3 && surf->numsurfedges <= TESS_MAX_VERTICES); + Q_assert(size >= surf->numsurfedges * VERTEX_SIZE * sizeof(vbo[0])); + size -= surf->numsurfedges * VERTEX_SIZE * sizeof(vbo[0]); if (gl_static.world.vertices) { vbo = gl_static.world.vertices + currvert * VERTEX_SIZE; @@ -1095,6 +1098,7 @@ void GL_LoadWorld(const char *name) gl_static.world.vertices = Z_TagMalloc(size, TAG_RENDERER); Com_DPrintf("%s: %zu bytes of vertex data on heap\n", __func__, size); } + gl_static.world.buffer_size = size; gl_static.nolm_mask = SURF_NOLM_MASK_DEFAULT; gl_static.use_bmodel_skies = false; From d3c1a67a0d5632ae8fa3375c75915b229226d5cf Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 17 Sep 2024 13:19:07 +0300 Subject: [PATCH 08/43] Don't register texinfo for SURF_NODRAW. Don't auto download it either. --- src/client/download.c | 2 ++ src/refresh/surf.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/client/download.c b/src/client/download.c index 031d551b3..dc5c99d4f 100644 --- a/src/client/download.c +++ b/src/client/download.c @@ -823,6 +823,8 @@ void CL_RequestNextDownload(void) if (allow_download_textures->integer) { for (i = 0; i < cl.bsp->numtexinfo; i++) { + if (cl.bsp->texinfo[i].c.flags & SURF_NODRAW) + continue; len = Q_concat(fn, sizeof(fn), "textures/", cl.bsp->texinfo[i].name, ".wal"); check_file_len(fn, len, DL_OTHER); } diff --git a/src/refresh/surf.c b/src/refresh/surf.c index ba0f25e8d..ff79651ad 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -1061,6 +1061,8 @@ void GL_LoadWorld(const char *name) } else { info->image = R_SKYTEXTURE; } + } else if (info->c.flags & SURF_NODRAW) { + info->image = R_NOTEXTURE; } else { imageflags_t flags = (info->c.flags & SURF_WARP) ? IF_TURBULENT : IF_NONE; Q_concat(buffer, sizeof(buffer), "textures/", info->name, ".wal"); From 84b4d6f79034ceea7fe422938633bcf4a4cc0a44 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 19 Sep 2024 12:19:08 +0300 Subject: [PATCH 09/43] Always enable blend for debug lines. --- src/refresh/debug.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/refresh/debug.c b/src/refresh/debug.c index 8449781e3..9398076ec 100644 --- a/src/refresh/debug.c +++ b/src/refresh/debug.c @@ -95,11 +95,9 @@ void R_AddDebugLine(const vec3_t start, const vec3_t end, uint32_t color, uint32 l->time = com_localTime2 + time; if (l->time < com_localTime2) l->time = UINT32_MAX; - l->bits = GLS_DEPTHMASK_FALSE; + l->bits = GLS_DEPTHMASK_FALSE | GLS_BLEND_BLEND; if (!depth_test) l->bits |= GLS_DEPTHTEST_DISABLE; - if (gl_config.caps & QGL_CAP_LINE_SMOOTH) - l->bits |= GLS_BLEND_BLEND; } #define GL_DRAWLINE(sx, sy, sz, ex, ey, ez) \ From 958598292de6d8b42468971fbf9fdaa4338f3d06 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 20 Sep 2024 18:59:04 +0300 Subject: [PATCH 10/43] Improve debug text drawing. Center text lines at origin. Frustum cull each line individually. Break oriented text into lines to allow for longer text. --- src/refresh/debug.c | 90 +++++++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 23 deletions(-) diff --git a/src/refresh/debug.c b/src/refresh/debug.c index 9398076ec..e7a7338e2 100644 --- a/src/refresh/debug.c +++ b/src/refresh/debug.c @@ -42,7 +42,7 @@ typedef struct { uint32_t color; uint32_t time; glStateBits_t bits; - char text[MAX_QPATH]; + char text[128]; } debug_text_t; static debug_text_t debug_texts[MAX_DEBUG_TEXTS]; @@ -325,9 +325,13 @@ void R_AddDebugCurveArrow(const vec3_t start, const vec3_t ctrl, const vec3_t en } } -void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char *text, - float size, uint32_t color, uint32_t time, qboolean depth_test) +static void R_AddDebugTextInternal(const vec3_t origin, const vec3_t angles, const char *text, + size_t len, float size, uint32_t color, uint32_t time, + qboolean depth_test) { + if (!len) + return; + debug_text_t *t = LIST_FIRST(debug_text_t, &debug_texts_free, entry); if (LIST_EMPTY(&debug_texts_free)) { @@ -367,7 +371,39 @@ void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char *text, t->bits |= GLS_DEPTHTEST_DISABLE; if (angles) t->bits |= GLS_CULL_DISABLE; - Q_strlcpy(t->text, text, sizeof(t->text)); + len = min(len, sizeof(t->text) - 1); + memcpy(t->text, text, len); + t->text[len] = 0; +} + +void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char *text, + float size, uint32_t color, uint32_t time, qboolean depth_test) +{ + vec3_t down, pos, up; + const char *s, *p; + + if (!angles) { + R_AddDebugTextInternal(origin, angles, text, strlen(text), size, color, time, depth_test); + return; + } + + AngleVectors(angles, NULL, NULL, up); + VectorScale(up, -size, down); + + VectorCopy(origin, pos); + + // break oriented text into lines to allow for longer text + s = text; + while (*s) { + p = strchr(s, '\n'); + if (!p) { + R_AddDebugTextInternal(pos, angles, s, strlen(s), size, color, time, depth_test); + break; + } + R_AddDebugTextInternal(pos, angles, s, p - s, size, color, time, depth_test); + VectorAdd(pos, down, pos); + s = p + 1; + } } static void GL_DrawDebugLines(void) @@ -504,6 +540,25 @@ static void GL_DrawDebugChar(const vec3_t pos, const vec3_t right, const vec3_t tess.flags = bits; } +static void GL_DrawDebugTextLine(const vec3_t origin, const vec3_t right, const vec3_t down, + const debug_text_t *text, const char *s, size_t len) +{ + // frustum cull + float radius = text->size * 0.5f * len; + for (int i = 0; i < 4; i++) + if (PlaneDiff(origin, &glr.frustumPlanes[i]) < -radius) + return; + + // draw it + vec3_t pos; + VectorMA(origin, -0.5f * len, right, pos); + while (*s && len--) { + byte c = *s++; + GL_DrawDebugChar(pos, right, down, text->bits, text->color, c); + VectorAdd(pos, right, pos); + } +} + static void GL_DrawDebugTexts(void) { debug_text_t *text, *next; @@ -516,9 +571,7 @@ static void GL_DrawDebugTexts(void) LIST_FOR_EACH_SAFE(debug_text_t, text, next, &debug_texts_active, entry) { vec3_t right, down, pos; - const char *s; - float radius; - int i; + const char *s, *p; if (text->time < com_localTime2) { // expired List_Remove(&text->entry); @@ -531,14 +584,6 @@ static void GL_DrawDebugTexts(void) if (text->size < DotProduct(pos, glr.viewaxis[0]) * gl_debug_distfrac->value) continue; - // frustum cull - radius = strlen(text->text) * text->size; - for (i = 0; i < 4; i++) - if (PlaneDiff(text->origin, &glr.frustumPlanes[i]) < -radius) - break; - if (i != 4) - continue; - if (text->bits & GLS_CULL_DISABLE) { // oriented vec3_t up; AngleVectors(text->angles, NULL, right, up); @@ -550,17 +595,16 @@ static void GL_DrawDebugTexts(void) } VectorCopy(text->origin, pos); - i = 0; s = text->text; while (*s) { - byte c = *s++; - if (c == '\n') { - i++; - VectorMA(text->origin, i, down, pos); - continue; + p = strchr(s, '\n'); + if (!p) { + GL_DrawDebugTextLine(pos, right, down, text, s, strlen(s)); + break; } - GL_DrawDebugChar(pos, right, down, text->bits, text->color, c); - VectorAdd(pos, right, pos); + GL_DrawDebugTextLine(pos, right, down, text, s, p - s); + VectorAdd(pos, down, pos); + s = p + 1; } } From 5ff82e3e2385893b08bd4fbe505f1b4a621d3913 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 21 Sep 2024 17:20:35 +0300 Subject: [PATCH 11/43] Disable cubemaps by default. They cause some performance hit. Better to enable them separately for re-release. --- doc/client.asciidoc | 4 ++-- src/refresh/texture.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 120be0ed6..c31ae79aa 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -680,8 +680,8 @@ gl_drawsky:: gl_cubemaps:: Enables use of cubemaps for skybox drawing. Cubemaps allow multiple skybox textures per map, and help to avoid sky rendering bugs. Also enables N64 - skies (although these aren't technically cubemaps). Only effective if - ‘gl_shaders’ is enabled. Default value is 1 (enabled). + skies in re-release maps. Only effective if ‘gl_shaders’ is enabled. + Default value is 0 (disabled). gl_waterwarp:: Enable screen warping effect when underwater. Only effective when using diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 4a5e501dd..13dc1ffdc 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -1229,7 +1229,7 @@ void GL_InitImages(void) gl_gamma = Cvar_Get("vid_gamma", "1", CVAR_ARCHIVE); gl_partshape = Cvar_Get("gl_partshape", "0", 0); gl_partshape->changed = gl_partshape_changed; - gl_cubemaps = Cvar_Get("gl_cubemaps", "1", CVAR_FILES); + gl_cubemaps = Cvar_Get("gl_cubemaps", "0", CVAR_FILES); if (r_config.flags & QVF_GAMMARAMP) { gl_gamma->changed = gl_gamma_changed; From 4208686974d5a2fb313bbcee7cb707ac151a740a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 22 Sep 2024 12:17:53 +0300 Subject: [PATCH 12/43] Fix double assignment. --- src/refresh/surf.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index ff79651ad..10b06a463 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -743,7 +743,7 @@ static void sample_surface_verts(mface_t *surf, vec_t *vbo) // normalizes and stores lightmap texture coordinates in vertices static void normalize_surface_lmtc(const mface_t *surf, vec_t *vbo) { - float s, t, scale = scale = 1.0f / lm.block_size; + float s, t, scale = 1.0f / lm.block_size; int i; s = surf->light_s + 0.5f; From f6fdb81b3850fc5f0cac9ab9abf15e5474623930 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 22 Sep 2024 12:35:05 +0300 Subject: [PATCH 13/43] Fix missing break statement. --- src/refresh/qgl.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 4c14938b8..486a02bc8 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -457,6 +457,7 @@ static bool parse_gl_version(void) if (!strncmp(s, es_prefixes[i], len)) { s += len; gl_es = true; + break; } } From 45b25f730df88312a18ec512f496fb9e6fbfc99c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 23 Sep 2024 23:09:55 +0300 Subject: [PATCH 14/43] Make hash map allocations tagged. --- inc/common/hash_map.h | 11 +++++++++-- inc/common/zone.h | 2 +- src/common/hash_map.c | 15 +++++++++------ src/common/zone.c | 4 +++- src/refresh/main.c | 2 +- src/refresh/shader.c | 2 +- src/refresh/texture.c | 2 +- 7 files changed, 25 insertions(+), 13 deletions(-) diff --git a/inc/common/hash_map.h b/inc/common/hash_map.h index 0d91b87bf..b409e9d3c 100644 --- a/inc/common/hash_map.h +++ b/inc/common/hash_map.h @@ -18,11 +18,16 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +#pragma once + +#include "common/zone.h" + typedef struct hash_map_s hash_map_t; hash_map_t *HashMap_CreateImpl(const uint32_t key_size, const uint32_t value_size, uint32_t (*hasher)(const void *const), - bool (*comp)(const void *const, const void *const)); + bool (*comp)(const void *const, const void *const), + memtag_t tag); void HashMap_Destroy(hash_map_t *map); void HashMap_Reserve(hash_map_t *map, uint32_t capacity); bool HashMap_InsertImpl(hash_map_t *map, const uint32_t key_size, const uint32_t value_size, const void *const key, const void *const value); @@ -32,7 +37,9 @@ uint32_t HashMap_Size(const hash_map_t *map); void *HashMap_GetKeyImpl(const hash_map_t *map, uint32_t index); void *HashMap_GetValueImpl(const hash_map_t *map, uint32_t index); -#define HashMap_Create(key_type, value_type, hasher, comp) HashMap_CreateImpl(sizeof(key_type), sizeof(value_type), hasher, comp) +#define HashMap_TagCreate(key_type, value_type, hasher, comp, tag) \ + HashMap_CreateImpl(sizeof(key_type), sizeof(value_type), hasher, comp, tag) +#define HashMap_Create(key_type, value_type, hasher, comp) HashMap_TagCreate(key_type, value_type, hasher, comp, TAG_GENERAL) #define HashMap_Insert(map, key, value) HashMap_InsertImpl(map, sizeof(*key), sizeof(*value), key, value) #define HashMap_Erase(map, key) HashMap_EraseImpl(map, sizeof(*key), key) #define HashMap_Lookup(type, map, key) ((type *)HashMap_LookupImpl(map, sizeof(*key), key)) diff --git a/inc/common/zone.h b/inc/common/zone.h index 0cada9337..774f15543 100644 --- a/inc/common/zone.h +++ b/inc/common/zone.h @@ -45,7 +45,7 @@ void Z_Init(void); void Z_Free(void *ptr); void Z_Freep(void *ptr); void *Z_Realloc(void *ptr, size_t size); -void *Z_ReallocArray(void *ptr, size_t nmemb, size_t size); +void *Z_ReallocArray(void *ptr, size_t nmemb, size_t size, memtag_t tag); q_malloc void *Z_Malloc(size_t size); q_malloc diff --git a/src/common/hash_map.c b/src/common/hash_map.c index 7d311acc1..42231e517 100644 --- a/src/common/hash_map.c +++ b/src/common/hash_map.c @@ -31,6 +31,7 @@ typedef struct hash_map_s { uint32_t key_value_storage_size; uint32_t key_size; uint32_t value_size; + memtag_t tag; uint32_t (*hasher)(const void *const); bool (*comp)(const void *const, const void *const); uint32_t *hash_to_index; @@ -69,7 +70,7 @@ static void HashMap_Rehash(hash_map_t *map, const uint32_t new_size) if (map->hash_size >= new_size) return; map->hash_size = new_size; - map->hash_to_index = Z_ReallocArray(map->hash_to_index, map->hash_size, sizeof(uint32_t)); + map->hash_to_index = Z_ReallocArray(map->hash_to_index, map->hash_size, sizeof(uint32_t), map->tag); memset(map->hash_to_index, 0xFF, map->hash_size * sizeof(uint32_t)); for (uint32_t i = 0; i < map->num_entries; ++i) { void *key = HashMap_GetKeyImpl(map, i); @@ -87,9 +88,9 @@ HashMap_ExpandKeyValueStorage */ static void HashMap_ExpandKeyValueStorage(hash_map_t *map, const uint32_t new_size) { - map->keys = Z_ReallocArray(map->keys, new_size, map->key_size); - map->values = Z_ReallocArray(map->values, new_size, map->value_size); - map->index_chain = Z_ReallocArray(map->index_chain, new_size, sizeof(uint32_t)); + map->keys = Z_ReallocArray(map->keys, new_size, map->key_size, map->tag); + map->values = Z_ReallocArray(map->values, new_size, map->value_size, map->tag); + map->index_chain = Z_ReallocArray(map->index_chain, new_size, sizeof(uint32_t), map->tag); map->key_value_storage_size = new_size; } @@ -100,13 +101,15 @@ HashMap_CreateImpl */ hash_map_t *HashMap_CreateImpl(const uint32_t key_size, const uint32_t value_size, uint32_t (*hasher)(const void *const), - bool (*comp)(const void *const, const void *const)) + bool (*comp)(const void *const, const void *const), + memtag_t tag) { - hash_map_t *map = Z_Mallocz(sizeof(*map)); + hash_map_t *map = Z_TagMallocz(sizeof(*map), tag); map->key_size = key_size; map->value_size = value_size; map->hasher = hasher; map->comp = comp; + map->tag = tag; return map; } diff --git a/src/common/zone.c b/src/common/zone.c index e5f941fb7..2eeee0d00 100644 --- a/src/common/zone.c +++ b/src/common/zone.c @@ -197,9 +197,11 @@ void *Z_Realloc(void *ptr, size_t size) return z + 1; } -void *Z_ReallocArray(void *ptr, size_t nmemb, size_t size) +void *Z_ReallocArray(void *ptr, size_t nmemb, size_t size, memtag_t tag) { Q_assert(!size || nmemb <= INT_MAX / size); + if (!ptr) + return Z_TagMalloc(nmemb * size, tag); return Z_Realloc(ptr, nmemb * size); } diff --git a/src/refresh/main.c b/src/refresh/main.c index 0b70faef3..804f74025 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1051,7 +1051,7 @@ void GL_InitQueries(void) gl_static.samples_passed = GL_ANY_SAMPLES_PASSED; Q_assert(!gl_static.queries); - gl_static.queries = HashMap_Create(int, glquery_t, HashInt32, NULL); + gl_static.queries = HashMap_TagCreate(int, glquery_t, HashInt32, NULL, TAG_RENDERER); } void GL_DeleteQueries(void) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index e216d97a1..c06060cd9 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -465,7 +465,7 @@ static void shader_clear_state(void) static void shader_init(void) { - gl_static.programs = HashMap_Create(glStateBits_t, GLuint, HashInt32, NULL); + gl_static.programs = HashMap_TagCreate(glStateBits_t, GLuint, HashInt32, NULL, TAG_RENDERER); qglGenBuffers(1, &gl_static.uniform_buffer); qglBindBuffer(GL_UNIFORM_BUFFER, gl_static.uniform_buffer); diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 13dc1ffdc..0e9e8771a 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -918,7 +918,7 @@ int IMG_ReadPixels(screenshot_t *s) s->bpp = bpp; s->rowbytes = rowbytes; - s->pixels = Z_Malloc(buf_size); + s->pixels = Z_TagMalloc(buf_size, TAG_RENDERER); s->width = r_config.width; s->height = r_config.height; From 0d674ae210731695996c85d7dd06191ab8726b85 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 15/43] Templatize BSP loading code. --- src/common/bsp.c | 557 +++----------------------------------- src/common/bsp_template.c | 549 +++++++++++++++++++++++++++++++++++++ 2 files changed, 581 insertions(+), 525 deletions(-) create mode 100644 src/common/bsp_template.c diff --git a/src/common/bsp.c b/src/common/bsp.c index 4a466c01f..4f93f3344 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -45,517 +45,20 @@ static cvar_t *map_visibility_patch; =============================================================================== */ -#define ALLOC(size) \ +#define BSP_ALLOC(size) \ Hunk_Alloc(&bsp->hunk, size) -#define LOAD(func) \ - static int BSP_Load##func(bsp_t *const bsp, const byte *in, \ - const size_t count, const bool extended) - -#define DEBUG(msg) \ +#define BSP_ERROR(msg) \ Com_SetLastError(va("%s: %s", __func__, msg)) -#define ENSURE(cond, msg) \ - do { if (!(cond)) { DEBUG(msg); return Q_ERR_INVALID_FORMAT; } } while (0) - -#define BSP_Short() (in += 2, RL16(in - 2)) -#define BSP_Long() (in += 4, RL32(in - 4)) -#define BSP_Float() LongToFloat(BSP_Long()) - -#define BSP_ExtFloat() (extended ? BSP_Float() : (int16_t)BSP_Short()) -#define BSP_ExtLong() (extended ? BSP_Long() : BSP_Short()) -#define BSP_ExtNull (extended ? (uint32_t)-1 : (uint16_t)-1) - -#define BSP_VectorAdd(v, add) \ - ((v)[0] = BSP_Float() add, (v)[1] = BSP_Float() add, (v)[2] = BSP_Float() add) - -#define BSP_ExtVector(v) \ - ((v)[0] = BSP_ExtFloat(), (v)[1] = BSP_ExtFloat(), (v)[2] = BSP_ExtFloat()) - -#define BSP_Vector(v) BSP_VectorAdd(v,) - -LOAD(Visibility) -{ - if (!count) - return Q_ERR_SUCCESS; - - ENSURE(count >= 4, "Too small header"); - - uint32_t numclusters = BSP_Long(); - ENSURE(numclusters <= MAX_MAP_CLUSTERS, "Too many clusters"); - - uint32_t hdrsize = 4 + numclusters * 8; - ENSURE(count >= hdrsize, "Too small header"); - - bsp->numvisibility = count; - bsp->vis = ALLOC(count); - bsp->vis->numclusters = numclusters; - bsp->visrowsize = (numclusters + 7) >> 3; - - for (int i = 0; i < numclusters; i++) { - for (int j = 0; j < 2; j++) { - uint32_t bitofs = BSP_Long(); - ENSURE(bitofs >= hdrsize && bitofs < count, "Bad bitofs"); - bsp->vis->bitofs[i][j] = bitofs; - } - } - - memcpy(bsp->vis->bitofs + numclusters, in, count - hdrsize); - - return Q_ERR_SUCCESS; -} - -LOAD(Texinfo) -{ - mtexinfo_t *out; - - bsp->numtexinfo = count; - bsp->texinfo = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { -#if USE_REF - for (int j = 0; j < 2; j++) { - BSP_Vector(out->axis[j]); - out->offset[j] = BSP_Float(); - } -#else - in += 32; -#endif - out->c.flags = BSP_Long(); - out->c.value = BSP_Long(); - - memcpy(out->c.name, in, sizeof(out->c.name) - 1); - memcpy(out->name, in, sizeof(out->name) - 1); - in += MAX_TEXNAME; - -#if USE_REF - int32_t next = (int32_t)BSP_Long(); - if (next > 0) { - ENSURE(next < count, "Bad anim chain"); - out->next = bsp->texinfo + next; - } else { - out->next = NULL; - } -#else - in += 4; -#endif - } - -#if USE_REF - // count animation frames - out = bsp->texinfo; - for (int i = 0; i < count; i++, out++) { - out->numframes = 1; - for (mtexinfo_t *step = out->next; step && step != out; step = step->next) { - if (out->numframes == count) { - DEBUG("Infinite anim chain"); - return Q_ERR_INFINITE_LOOP; - } - out->numframes++; - } - } -#endif - - return Q_ERR_SUCCESS; -} - -LOAD(Planes) -{ - cplane_t *out; - - bsp->numplanes = count; - bsp->planes = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, in += 4, out++) { - BSP_Vector(out->normal); - out->dist = BSP_Float(); - SetPlaneType(out); - SetPlaneSignbits(out); - } - - return Q_ERR_SUCCESS; -} - -LOAD(BrushSides) -{ - mbrushside_t *out; - - bsp->numbrushsides = count; - bsp->brushsides = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t planenum = BSP_ExtLong(); - ENSURE(planenum < bsp->numplanes, "Bad planenum"); - out->plane = bsp->planes + planenum; - - uint32_t texinfo = BSP_ExtLong(); - if (texinfo == BSP_ExtNull) { - out->texinfo = &nulltexinfo; - } else { - ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); - out->texinfo = bsp->texinfo + texinfo; - } - } - - return Q_ERR_SUCCESS; -} - -LOAD(Brushes) -{ - mbrush_t *out; - - bsp->numbrushes = count; - bsp->brushes = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t firstside = BSP_Long(); - uint32_t numsides = BSP_Long(); - ENSURE((uint64_t)firstside + numsides <= bsp->numbrushsides, "Bad brushsides"); - out->firstbrushside = bsp->brushsides + firstside; - out->numsides = numsides; - out->contents = BSP_Long(); - out->checkcount = 0; - } - - return Q_ERR_SUCCESS; -} - -LOAD(LeafBrushes) -{ - mbrush_t **out; - - bsp->numleafbrushes = count; - bsp->leafbrushes = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t brushnum = BSP_ExtLong(); - ENSURE(brushnum < bsp->numbrushes, "Bad brushnum"); - *out = bsp->brushes + brushnum; - } - - return Q_ERR_SUCCESS; -} - - -#if USE_REF -LOAD(Lightmap) -{ - if (count) { - bsp->numlightmapbytes = count; - bsp->lightmap = ALLOC(count); - memcpy(bsp->lightmap, in, count); - } - - return Q_ERR_SUCCESS; -} - -LOAD(Vertices) -{ - mvertex_t *out; - - bsp->numvertices = count; - bsp->vertices = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) - BSP_Vector(out->point); - - return Q_ERR_SUCCESS; -} - -LOAD(Edges) -{ - medge_t *out; - - bsp->numedges = count; - bsp->edges = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - for (int j = 0; j < 2; j++) { - uint32_t vertnum = BSP_ExtLong(); - ENSURE(vertnum < bsp->numvertices, "Bad vertnum"); - out->v[j] = vertnum; - } - } - - return Q_ERR_SUCCESS; -} - -LOAD(SurfEdges) -{ - msurfedge_t *out; - - bsp->numsurfedges = count; - bsp->surfedges = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t index = BSP_Long(); - uint32_t vert = index >> 31; - if (vert) - index = -index; - ENSURE(index < bsp->numedges, "Bad edgenum"); - out->edge = index; - out->vert = vert; - } - - return Q_ERR_SUCCESS; -} - -LOAD(Faces) -{ - mface_t *out; - - bsp->numfaces = count; - bsp->faces = out = ALLOC(sizeof(*out) * count); - - for (int i = 0, j; i < count; i++, out++) { - uint32_t planenum = BSP_ExtLong(); - ENSURE(planenum < bsp->numplanes, "Bad planenum"); - out->plane = bsp->planes + planenum; - - out->drawflags = BSP_ExtLong() & DSURF_PLANEBACK; - - uint32_t firstedge = BSP_Long(); - uint32_t numedges = BSP_ExtLong(); - ENSURE(numedges >= 3 && numedges <= 4096 && - (uint64_t)firstedge + numedges <= bsp->numsurfedges, "Bad surfedges"); - out->firstsurfedge = bsp->surfedges + firstedge; - out->numsurfedges = numedges; - - uint32_t texinfo = BSP_ExtLong(); - ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); - out->texinfo = bsp->texinfo + texinfo; - - for (j = 0; j < MAX_LIGHTMAPS && in[j] != 255; j++) - out->styles[j] = in[j]; - - for (out->numstyles = j; j < MAX_LIGHTMAPS; j++) - out->styles[j] = 255; - - in += MAX_LIGHTMAPS; - - uint32_t lightofs = BSP_Long(); - if (lightofs == (uint32_t)-1 || bsp->numlightmapbytes == 0) { - out->lightmap = NULL; - } else { - ENSURE(lightofs < bsp->numlightmapbytes, "Bad lightofs"); - out->lightmap = bsp->lightmap + lightofs; - } - } - - return Q_ERR_SUCCESS; -} - -LOAD(LeafFaces) -{ - mface_t **out; - - bsp->numleaffaces = count; - bsp->leaffaces = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t facenum = BSP_ExtLong(); - ENSURE(facenum < bsp->numfaces, "Bad facenum"); - *out = bsp->faces + facenum; - } - - return Q_ERR_SUCCESS; -} -#endif - -LOAD(Leafs) -{ - mleaf_t *out; - - ENSURE(count > 0, "Map with no leafs"); - - bsp->numleafs = count; - bsp->leafs = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - out->plane = NULL; - out->contents = BSP_Long(); - - uint32_t cluster = BSP_ExtLong(); - if (cluster == BSP_ExtNull) { - // solid leafs use special -1 cluster - out->cluster = -1; - } else if (bsp->vis == NULL) { - // map has no vis, use 0 as a default cluster - out->cluster = 0; - } else { - // validate cluster - ENSURE(cluster < bsp->vis->numclusters, "Bad cluster"); - out->cluster = cluster; - } - - uint32_t area = BSP_ExtLong(); - ENSURE(area < bsp->numareas, "Bad area"); - out->area = area; - -#if USE_REF - BSP_ExtVector(out->mins); - BSP_ExtVector(out->maxs); - uint32_t firstleafface = BSP_ExtLong(); - uint32_t numleaffaces = BSP_ExtLong(); - ENSURE((uint64_t)firstleafface + numleaffaces <= bsp->numleaffaces, "Bad leaffaces"); - out->firstleafface = bsp->leaffaces + firstleafface; - out->numleaffaces = numleaffaces; - - out->parent = NULL; - out->visframe = -1; -#else - in += 16 * (bsp->extended + 1); -#endif - - uint32_t firstleafbrush = BSP_ExtLong(); - uint32_t numleafbrushes = BSP_ExtLong(); - ENSURE((uint64_t)firstleafbrush + numleafbrushes <= bsp->numleafbrushes, "Bad leafbrushes"); - out->firstleafbrush = bsp->leafbrushes + firstleafbrush; - out->numleafbrushes = numleafbrushes; - } - - ENSURE(bsp->leafs[0].contents == CONTENTS_SOLID, "Map leaf 0 is not CONTENTS_SOLID"); - - return Q_ERR_SUCCESS; -} - -LOAD(Nodes) -{ - mnode_t *out; - - ENSURE(count > 0, "Map with no nodes"); - - bsp->numnodes = count; - bsp->nodes = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t planenum = BSP_Long(); - ENSURE(planenum < bsp->numplanes, "Bad planenum"); - out->plane = bsp->planes + planenum; - - for (int j = 0; j < 2; j++) { - uint32_t child = BSP_Long(); - if (child & BIT(31)) { - child = ~child; - ENSURE(child < bsp->numleafs, "Bad leafnum"); - out->children[j] = (mnode_t *)(bsp->leafs + child); - } else { - ENSURE(child < count, "Bad nodenum"); - out->children[j] = bsp->nodes + child; - } - } - -#if USE_REF - BSP_ExtVector(out->mins); - BSP_ExtVector(out->maxs); - uint32_t firstface = BSP_ExtLong(); - uint32_t numfaces = BSP_ExtLong(); - ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); - out->firstface = bsp->faces + firstface; - out->numfaces = numfaces; - - out->parent = NULL; - out->visframe = -1; -#else - in += 16 * (bsp->extended + 1); -#endif - } - - return Q_ERR_SUCCESS; -} - -LOAD(SubModels) -{ - mmodel_t *out; - - ENSURE(count > 0, "Map with no models"); - ENSURE(count <= MAX_MODELS - 2, "Too many models"); - - bsp->nummodels = count; - bsp->models = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - // spread the mins / maxs by a pixel - BSP_VectorAdd(out->mins, -1); - BSP_VectorAdd(out->maxs, +1); - BSP_Vector(out->origin); - - uint32_t headnode = BSP_Long(); - if (headnode & BIT(31)) { - // be careful, some models have no nodes, just a leaf - headnode = ~headnode; - ENSURE(headnode < bsp->numleafs, "Bad headleaf"); - out->headnode = (mnode_t *)(bsp->leafs + headnode); - } else { - ENSURE(headnode < bsp->numnodes, "Bad headnode"); - out->headnode = bsp->nodes + headnode; - } -#if USE_REF - if (i == 0) { - in += 8; - continue; - } - uint32_t firstface = BSP_Long(); - uint32_t numfaces = BSP_Long(); - ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); - out->firstface = bsp->faces + firstface; - out->numfaces = numfaces; - - out->radius = RadiusFromBounds(out->mins, out->maxs); -#else - in += 8; -#endif - } - - return Q_ERR_SUCCESS; -} - -// These are validated after all the areas are loaded -LOAD(AreaPortals) -{ - mareaportal_t *out; - - bsp->numareaportals = count; - bsp->areaportals = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - out->portalnum = BSP_Long(); - out->otherarea = BSP_Long(); - } - - return Q_ERR_SUCCESS; -} - -LOAD(Areas) -{ - marea_t *out; - - ENSURE(count <= MAX_MAP_AREAS, "Too many areas"); - - bsp->numareas = count; - bsp->areas = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t numareaportals = BSP_Long(); - uint32_t firstareaportal = BSP_Long(); - ENSURE((uint64_t)firstareaportal + numareaportals <= bsp->numareaportals, "Bad areaportals"); - out->numareaportals = numareaportals; - out->firstareaportal = bsp->areaportals + firstareaportal; - out->floodvalid = 0; - } - - return Q_ERR_SUCCESS; -} +#define BSP_ENSURE(cond, msg) \ + do { if (!(cond)) { BSP_ERROR(msg); return Q_ERR_INVALID_FORMAT; } } while (0) -LOAD(EntString) -{ - bsp->numentitychars = count; - bsp->entitystring = ALLOC(count + 1); - memcpy(bsp->entitystring, in, count); - bsp->entitystring[count] = 0; +#define BSP_EXTENDED 0 +#include "bsp_template.c" - return Q_ERR_SUCCESS; -} +#define BSP_EXTENDED 1 +#include "bsp_template.c" /* =============================================================================== @@ -572,7 +75,7 @@ typedef struct { } xlump_info_t; typedef struct { - int (*load)(bsp_t *const, const byte *, const size_t, const bool); + int (*load[2])(bsp_t *const, const byte *, const size_t); const char *name; uint8_t lump; uint8_t disksize[2]; @@ -585,32 +88,36 @@ typedef struct { } bsp_stat_t; #define L(name, lump, mem_t, disksize1, disksize2) \ - { BSP_Load##name, #name, lump, { disksize1, disksize2 }, sizeof(mem_t) } + { { BSP_Load##name, BSP_Load##name }, #name, lump, { disksize1, disksize2 }, sizeof(mem_t) } + +#define E(name, lump, mem_t, disksize1, disksize2) \ + { { BSP_Load##name, BSP_Load##name##Ext }, #name, lump, { disksize1, disksize2 }, sizeof(mem_t) } static const lump_info_t bsp_lumps[] = { L(Visibility, 3, byte, 1, 1), L(Texinfo, 5, mtexinfo_t, 76, 76), L(Planes, 1, cplane_t, 20, 20), - L(BrushSides, 15, mbrushside_t, 4, 8), + E(BrushSides, 15, mbrushside_t, 4, 8), L(Brushes, 14, mbrush_t, 12, 12), - L(LeafBrushes, 10, mbrush_t *, 2, 4), + E(LeafBrushes, 10, mbrush_t *, 2, 4), L(AreaPortals, 18, mareaportal_t, 8, 8), L(Areas, 17, marea_t, 8, 8), #if USE_REF L(Lightmap, 7, byte, 1, 1), L(Vertices, 2, mvertex_t, 12, 12), - L(Edges, 11, medge_t, 4, 8), + E(Edges, 11, medge_t, 4, 8), L(SurfEdges, 12, msurfedge_t, 4, 4), - L(Faces, 6, mface_t, 20, 28), - L(LeafFaces, 9, mface_t *, 2, 4), + E(Faces, 6, mface_t, 20, 28), + E(LeafFaces, 9, mface_t *, 2, 4), #endif - L(Leafs, 8, mleaf_t, 28, 52), - L(Nodes, 4, mnode_t, 28, 44), + E(Leafs, 8, mleaf_t, 28, 52), + E(Nodes, 4, mnode_t, 28, 44), L(SubModels, 13, mmodel_t, 48, 48), L(EntString, 0, char, 1, 1), }; #undef L +#undef E #define F(x) { q_offsetof(bsp_t, num##x), #x } @@ -731,7 +238,7 @@ static int BSP_SetParent(mnode_t *node, unsigned key) // a face may never belong to more than one node for (i = 0, face = node->firstface; i < node->numfaces; i++, face++) { if (face->drawframe) { - DEBUG("Duplicate face"); + BSP_ERROR("Duplicate face"); return Q_ERR_INFINITE_LOOP; } face->drawframe = key; @@ -740,7 +247,7 @@ static int BSP_SetParent(mnode_t *node, unsigned key) child = node->children[0]; if (child->parent) { - DEBUG("Cycle encountered"); + BSP_ERROR("Cycle encountered"); return Q_ERR_INFINITE_LOOP; } child->parent = node; @@ -750,7 +257,7 @@ static int BSP_SetParent(mnode_t *node, unsigned key) child = node->children[1]; if (child->parent) { - DEBUG("Cycle encountered"); + BSP_ERROR("Cycle encountered"); return Q_ERR_INFINITE_LOOP; } child->parent = node; @@ -771,7 +278,7 @@ static int BSP_ValidateTree(bsp_t *bsp) for (i = 0, mod = bsp->models; i < bsp->nummodels; i++, mod++) { if (i == 0 && mod->headnode != bsp->nodes) { - DEBUG("Map model 0 headnode is not the first node"); + BSP_ERROR("Map model 0 headnode is not the first node"); return Q_ERR_INVALID_FORMAT; } @@ -784,7 +291,7 @@ static int BSP_ValidateTree(bsp_t *bsp) // a face may never belong to more than one model for (j = 0, face = mod->firstface; j < mod->numfaces; j++, face++) { if (face->drawframe && face->drawframe != ~i) { - DEBUG("Duplicate face"); + BSP_ERROR("Duplicate face"); return Q_ERR_INFINITE_LOOP; } face->drawframe = ~i; @@ -804,8 +311,8 @@ static int BSP_ValidateAreaPortals(bsp_t *bsp) bsp->numportals = 0; for (i = 0, p = bsp->areaportals; i < bsp->numareaportals; i++, p++) { - ENSURE(p->portalnum < bsp->numareaportals, "Bad portalnum"); - ENSURE(p->otherarea < bsp->numareas, "Bad otherarea"); + BSP_ENSURE(p->portalnum < bsp->numareaportals, "Bad portalnum"); + BSP_ENSURE(p->otherarea < bsp->numareas, "Bad otherarea"); bsp->numportals = max(bsp->numportals, p->portalnum + 1); } @@ -1082,7 +589,7 @@ static void BSP_ParseLightgrid(bsp_t *bsp, const byte *in, size_t filelen) SZ_InitRead(&s, in, filelen); - grid->nodes = ALLOC(sizeof(grid->nodes[0]) * grid->numnodes); + grid->nodes = BSP_ALLOC(sizeof(grid->nodes[0]) * grid->numnodes); // load children first s.readcount = 45; @@ -1107,11 +614,11 @@ static void BSP_ParseLightgrid(bsp_t *bsp, const byte *in, size_t filelen) s.readcount += 32; } - grid->leafs = ALLOC(sizeof(grid->leafs[0]) * grid->numleafs); + grid->leafs = BSP_ALLOC(sizeof(grid->leafs[0]) * grid->numleafs); // init samples to fully occluded size = sizeof(grid->samples[0]) * grid->numsamples * grid->numstyles; - grid->samples = sample = memset(ALLOC(size), 255, size); + grid->samples = sample = memset(BSP_ALLOC(size), 255, size); remaining = grid->numsamples; s.readcount += 4; @@ -1319,7 +826,7 @@ int BSP_Load(const char *name, bsp_t **bsp_p) // load all lumps for (i = 0; i < q_countof(bsp_lumps); i++) { - ret = bsp_lumps[i].load(bsp, buf + lump_ofs[i], lump_count[i], extended); + ret = bsp_lumps[i].load[extended](bsp, buf + lump_ofs[i], lump_count[i]); if (ret) { goto fail1; } diff --git a/src/common/bsp_template.c b/src/common/bsp_template.c new file mode 100644 index 000000000..9554f7233 --- /dev/null +++ b/src/common/bsp_template.c @@ -0,0 +1,549 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. +Copyright (C) 2008 Andrey Nazarov + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +// This file must be included twice, first with BSP_EXTENDED = 0 and then +// with BSP_EXTENDED = 1 (strictly in this order). +// +// This code doesn't use structs to allow for unaligned lumps reading. + +#if BSP_EXTENDED + +#undef BSP_LOAD +#undef BSP_ExtFloat +#undef BSP_ExtLong +#undef BSP_ExtNull + +#define BSP_LOAD(func) \ + static int BSP_Load##func##Ext(bsp_t *const bsp, const byte *in, const size_t count) + +#define BSP_ExtFloat() BSP_Float() +#define BSP_ExtLong() BSP_Long() +#define BSP_ExtNull (uint32_t)-1 + +#else + +#define BSP_Short() (in += 2, RL16(in - 2)) +#define BSP_Long() (in += 4, RL32(in - 4)) +#define BSP_Float() LongToFloat(BSP_Long()) + +#define BSP_LOAD(func) \ + static int BSP_Load##func(bsp_t *const bsp, const byte *in, const size_t count) + +#define BSP_ExtFloat() (int16_t)BSP_Short() +#define BSP_ExtLong() BSP_Short() +#define BSP_ExtNull (uint16_t)-1 + +#define BSP_Vector(v) \ + ((v)[0] = BSP_Float(), (v)[1] = BSP_Float(), (v)[2] = BSP_Float()) + +#define BSP_ExtVector(v) \ + ((v)[0] = BSP_ExtFloat(), (v)[1] = BSP_ExtFloat(), (v)[2] = BSP_ExtFloat()) + +BSP_LOAD(Visibility) +{ + if (!count) + return Q_ERR_SUCCESS; + + BSP_ENSURE(count >= 4, "Too small header"); + + uint32_t numclusters = BSP_Long(); + BSP_ENSURE(numclusters <= MAX_MAP_CLUSTERS, "Too many clusters"); + + uint32_t hdrsize = 4 + numclusters * 8; + BSP_ENSURE(count >= hdrsize, "Too small header"); + + bsp->numvisibility = count; + bsp->vis = BSP_ALLOC(count); + bsp->vis->numclusters = numclusters; + bsp->visrowsize = (numclusters + 7) >> 3; + + for (int i = 0; i < numclusters; i++) { + for (int j = 0; j < 2; j++) { + uint32_t bitofs = BSP_Long(); + BSP_ENSURE(bitofs >= hdrsize && bitofs < count, "Bad bitofs"); + bsp->vis->bitofs[i][j] = bitofs; + } + } + + memcpy(bsp->vis->bitofs + numclusters, in, count - hdrsize); + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Texinfo) +{ + mtexinfo_t *out; + + bsp->numtexinfo = count; + bsp->texinfo = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { +#if USE_REF + for (int j = 0; j < 2; j++) { + BSP_Vector(out->axis[j]); + out->offset[j] = BSP_Float(); + } +#else + in += 32; +#endif + out->c.flags = BSP_Long(); + out->c.value = BSP_Long(); + + memcpy(out->c.name, in, sizeof(out->c.name) - 1); + memcpy(out->name, in, sizeof(out->name) - 1); + in += MAX_TEXNAME; + +#if USE_REF + int32_t next = (int32_t)BSP_Long(); + if (next > 0) { + BSP_ENSURE(next < count, "Bad anim chain"); + out->next = bsp->texinfo + next; + } else { + out->next = NULL; + } +#else + in += 4; +#endif + } + +#if USE_REF + // count animation frames + out = bsp->texinfo; + for (int i = 0; i < count; i++, out++) { + out->numframes = 1; + for (mtexinfo_t *step = out->next; step && step != out; step = step->next) { + if (out->numframes == count) { + BSP_ERROR("Infinite anim chain"); + return Q_ERR_INFINITE_LOOP; + } + out->numframes++; + } + } +#endif + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Planes) +{ + cplane_t *out; + + bsp->numplanes = count; + bsp->planes = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, in += 4, out++) { + BSP_Vector(out->normal); + out->dist = BSP_Float(); + SetPlaneType(out); + SetPlaneSignbits(out); + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Brushes) +{ + mbrush_t *out; + + bsp->numbrushes = count; + bsp->brushes = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t firstside = BSP_Long(); + uint32_t numsides = BSP_Long(); + BSP_ENSURE((uint64_t)firstside + numsides <= bsp->numbrushsides, "Bad brushsides"); + out->firstbrushside = bsp->brushsides + firstside; + out->numsides = numsides; + out->contents = BSP_Long(); + out->checkcount = 0; + } + + return Q_ERR_SUCCESS; +} + +#if USE_REF +BSP_LOAD(Lightmap) +{ + if (count) { + bsp->numlightmapbytes = count; + bsp->lightmap = BSP_ALLOC(count); + memcpy(bsp->lightmap, in, count); + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Vertices) +{ + mvertex_t *out; + + bsp->numvertices = count; + bsp->vertices = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) + BSP_Vector(out->point); + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(SurfEdges) +{ + msurfedge_t *out; + + bsp->numsurfedges = count; + bsp->surfedges = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t index = BSP_Long(); + uint32_t vert = index >> 31; + if (vert) + index = -index; + BSP_ENSURE(index < bsp->numedges, "Bad edgenum"); + out->edge = index; + out->vert = vert; + } + + return Q_ERR_SUCCESS; +} +#endif + +BSP_LOAD(SubModels) +{ + mmodel_t *out; + + BSP_ENSURE(count > 0, "Map with no models"); + BSP_ENSURE(count <= MAX_MODELS - 2, "Too many models"); + + bsp->nummodels = count; + bsp->models = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + BSP_Vector(out->mins); + BSP_Vector(out->maxs); + BSP_Vector(out->origin); + + // spread the mins / maxs by a pixel + for (int j = 0; j < 3; j++) { + out->mins[j] -= 1; + out->maxs[j] += 1; + } + + uint32_t headnode = BSP_Long(); + if (headnode & BIT(31)) { + // be careful, some models have no nodes, just a leaf + headnode = ~headnode; + BSP_ENSURE(headnode < bsp->numleafs, "Bad headleaf"); + out->headnode = (mnode_t *)(bsp->leafs + headnode); + } else { + BSP_ENSURE(headnode < bsp->numnodes, "Bad headnode"); + out->headnode = bsp->nodes + headnode; + } +#if USE_REF + if (i == 0) { + in += 8; + continue; + } + uint32_t firstface = BSP_Long(); + uint32_t numfaces = BSP_Long(); + BSP_ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); + out->firstface = bsp->faces + firstface; + out->numfaces = numfaces; + + out->radius = RadiusFromBounds(out->mins, out->maxs); +#else + in += 8; +#endif + } + + return Q_ERR_SUCCESS; +} + +// These are validated after all the areas are loaded +BSP_LOAD(AreaPortals) +{ + mareaportal_t *out; + + bsp->numareaportals = count; + bsp->areaportals = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + out->portalnum = BSP_Long(); + out->otherarea = BSP_Long(); + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Areas) +{ + marea_t *out; + + BSP_ENSURE(count <= MAX_MAP_AREAS, "Too many areas"); + + bsp->numareas = count; + bsp->areas = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t numareaportals = BSP_Long(); + uint32_t firstareaportal = BSP_Long(); + BSP_ENSURE((uint64_t)firstareaportal + numareaportals <= bsp->numareaportals, "Bad areaportals"); + out->numareaportals = numareaportals; + out->firstareaportal = bsp->areaportals + firstareaportal; + out->floodvalid = 0; + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(EntString) +{ + bsp->numentitychars = count; + bsp->entitystring = BSP_ALLOC(count + 1); + memcpy(bsp->entitystring, in, count); + bsp->entitystring[count] = 0; + + return Q_ERR_SUCCESS; +} + +#endif // !BSP_EXTENDED + +BSP_LOAD(BrushSides) +{ + mbrushside_t *out; + + bsp->numbrushsides = count; + bsp->brushsides = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t planenum = BSP_ExtLong(); + BSP_ENSURE(planenum < bsp->numplanes, "Bad planenum"); + out->plane = bsp->planes + planenum; + + uint32_t texinfo = BSP_ExtLong(); + if (texinfo == BSP_ExtNull) { + out->texinfo = &nulltexinfo; + } else { + BSP_ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); + out->texinfo = bsp->texinfo + texinfo; + } + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(LeafBrushes) +{ + mbrush_t **out; + + bsp->numleafbrushes = count; + bsp->leafbrushes = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t brushnum = BSP_ExtLong(); + BSP_ENSURE(brushnum < bsp->numbrushes, "Bad brushnum"); + *out = bsp->brushes + brushnum; + } + + return Q_ERR_SUCCESS; +} + +#if USE_REF +BSP_LOAD(Edges) +{ + medge_t *out; + + bsp->numedges = count; + bsp->edges = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + for (int j = 0; j < 2; j++) { + uint32_t vertnum = BSP_ExtLong(); + BSP_ENSURE(vertnum < bsp->numvertices, "Bad vertnum"); + out->v[j] = vertnum; + } + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Faces) +{ + mface_t *out; + + bsp->numfaces = count; + bsp->faces = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0, j; i < count; i++, out++) { + uint32_t planenum = BSP_ExtLong(); + BSP_ENSURE(planenum < bsp->numplanes, "Bad planenum"); + out->plane = bsp->planes + planenum; + + out->drawflags = BSP_ExtLong() & DSURF_PLANEBACK; + + uint32_t firstedge = BSP_Long(); + uint32_t numedges = BSP_ExtLong(); + BSP_ENSURE(numedges >= 3 && numedges <= 4096 && + (uint64_t)firstedge + numedges <= bsp->numsurfedges, "Bad surfedges"); + out->firstsurfedge = bsp->surfedges + firstedge; + out->numsurfedges = numedges; + + uint32_t texinfo = BSP_ExtLong(); + BSP_ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); + out->texinfo = bsp->texinfo + texinfo; + + for (j = 0; j < MAX_LIGHTMAPS && in[j] != 255; j++) + out->styles[j] = in[j]; + + for (out->numstyles = j; j < MAX_LIGHTMAPS; j++) + out->styles[j] = 255; + + in += MAX_LIGHTMAPS; + + uint32_t lightofs = BSP_Long(); + if (lightofs == (uint32_t)-1 || bsp->numlightmapbytes == 0) { + out->lightmap = NULL; + } else { + BSP_ENSURE(lightofs < bsp->numlightmapbytes, "Bad lightofs"); + out->lightmap = bsp->lightmap + lightofs; + } + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(LeafFaces) +{ + mface_t **out; + + bsp->numleaffaces = count; + bsp->leaffaces = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t facenum = BSP_ExtLong(); + BSP_ENSURE(facenum < bsp->numfaces, "Bad facenum"); + *out = bsp->faces + facenum; + } + + return Q_ERR_SUCCESS; +} +#endif + +BSP_LOAD(Leafs) +{ + mleaf_t *out; + + BSP_ENSURE(count > 0, "Map with no leafs"); + + bsp->numleafs = count; + bsp->leafs = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + out->plane = NULL; + out->contents = BSP_Long(); + + uint32_t cluster = BSP_ExtLong(); + if (cluster == BSP_ExtNull) { + // solid leafs use special -1 cluster + out->cluster = -1; + } else if (bsp->vis == NULL) { + // map has no vis, use 0 as a default cluster + out->cluster = 0; + } else { + // validate cluster + BSP_ENSURE(cluster < bsp->vis->numclusters, "Bad cluster"); + out->cluster = cluster; + } + + uint32_t area = BSP_ExtLong(); + BSP_ENSURE(area < bsp->numareas, "Bad area"); + out->area = area; + +#if USE_REF + BSP_ExtVector(out->mins); + BSP_ExtVector(out->maxs); + uint32_t firstleafface = BSP_ExtLong(); + uint32_t numleaffaces = BSP_ExtLong(); + BSP_ENSURE((uint64_t)firstleafface + numleaffaces <= bsp->numleaffaces, "Bad leaffaces"); + out->firstleafface = bsp->leaffaces + firstleafface; + out->numleaffaces = numleaffaces; + + out->parent = NULL; + out->visframe = -1; +#else + in += 16 * (BSP_EXTENDED + 1); +#endif + + uint32_t firstleafbrush = BSP_ExtLong(); + uint32_t numleafbrushes = BSP_ExtLong(); + BSP_ENSURE((uint64_t)firstleafbrush + numleafbrushes <= bsp->numleafbrushes, "Bad leafbrushes"); + out->firstleafbrush = bsp->leafbrushes + firstleafbrush; + out->numleafbrushes = numleafbrushes; + } + + BSP_ENSURE(bsp->leafs[0].contents == CONTENTS_SOLID, "Map leaf 0 is not CONTENTS_SOLID"); + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Nodes) +{ + mnode_t *out; + + BSP_ENSURE(count > 0, "Map with no nodes"); + + bsp->numnodes = count; + bsp->nodes = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t planenum = BSP_Long(); + BSP_ENSURE(planenum < bsp->numplanes, "Bad planenum"); + out->plane = bsp->planes + planenum; + + for (int j = 0; j < 2; j++) { + uint32_t child = BSP_Long(); + if (child & BIT(31)) { + child = ~child; + BSP_ENSURE(child < bsp->numleafs, "Bad leafnum"); + out->children[j] = (mnode_t *)(bsp->leafs + child); + } else { + BSP_ENSURE(child < count, "Bad nodenum"); + out->children[j] = bsp->nodes + child; + } + } + +#if USE_REF + BSP_ExtVector(out->mins); + BSP_ExtVector(out->maxs); + uint32_t firstface = BSP_ExtLong(); + uint32_t numfaces = BSP_ExtLong(); + BSP_ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); + out->firstface = bsp->faces + firstface; + out->numfaces = numfaces; + + out->parent = NULL; + out->visframe = -1; +#else + in += 16 * (BSP_EXTENDED + 1); +#endif + } + + return Q_ERR_SUCCESS; +} + +#undef BSP_EXTENDED From 9593d90d4337fd7344f2e39097bdaae6c8660465 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 16/43] Update libjpeg-turbo to 3.0.4. --- subprojects/libjpeg-turbo.wrap | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/subprojects/libjpeg-turbo.wrap b/subprojects/libjpeg-turbo.wrap index 71f38a08c..f9eda5fb3 100644 --- a/subprojects/libjpeg-turbo.wrap +++ b/subprojects/libjpeg-turbo.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = libjpeg-turbo-3.0.3 -source_url = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.3/libjpeg-turbo-3.0.3.tar.gz -source_filename = libjpeg-turbo-3.0.3.tar.gz -source_hash = 343e789069fc7afbcdfe44dbba7dbbf45afa98a15150e079a38e60e44578865d -patch_filename = libjpeg-turbo_3.0.3-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libjpeg-turbo_3.0.3-1/get_patch -patch_hash = 8bbf4f205e54e73c48c14dae7fcc71f152cdc8fea6d55a1af1e3108d10c6a2e4 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libjpeg-turbo_3.0.3-1/libjpeg-turbo-3.0.3.tar.gz -wrapdb_version = 3.0.3-1 +directory = libjpeg-turbo-3.0.4 +source_url = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.4/libjpeg-turbo-3.0.4.tar.gz +source_filename = libjpeg-turbo-3.0.4.tar.gz +source_hash = 99130559e7d62e8d695f2c0eaeef912c5828d5b84a0537dcb24c9678c9d5b76b +patch_filename = libjpeg-turbo_3.0.4-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libjpeg-turbo_3.0.4-2/get_patch +patch_hash = f12d14c6ff4ae83eddc1a2e8cfd96a9b7a40b6bdc8ff345ca8094abf4c3da928 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libjpeg-turbo_3.0.4-2/libjpeg-turbo-3.0.4.tar.gz +wrapdb_version = 3.0.4-2 [provide] dependency_names = libjpeg, libturbojpeg From 61636aa512495b9a8363ce4e3b21ee8e61186f25 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 17/43] Update libpng to 1.6.44. --- subprojects/libpng.wrap | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/subprojects/libpng.wrap b/subprojects/libpng.wrap index d32e859a4..6632152df 100644 --- a/subprojects/libpng.wrap +++ b/subprojects/libpng.wrap @@ -1,12 +1,12 @@ [wrap-file] -directory = libpng-1.6.43 -source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.43.tar.xz -source_filename = libpng-1.6.43.tar.xz -source_hash = 6a5ca0652392a2d7c9db2ae5b40210843c0bbc081cbd410825ab00cc59f14a6c -patch_filename = libpng_1.6.43-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.43-2/get_patch -patch_hash = 49951297edf03e81d925ab03726555f09994ad1ed78fb539a269216430eef3da -wrapdb_version = 1.6.43-2 +directory = libpng-1.6.44 +source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.44.tar.xz +source_filename = libpng-1.6.44.tar.xz +source_hash = 60c4da1d5b7f0aa8d158da48e8f8afa9773c1c8baa5d21974df61f1886b8ce8e +patch_filename = libpng_1.6.44-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.44-1/get_patch +patch_hash = 394b07614c45fbd1beac8b660386216a490fe12f841a1a445799b676c9c892fb +wrapdb_version = 1.6.44-1 [provide] libpng = libpng_dep From 53374e678e74aced9b7b40695063cbc529de7074 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 18/43] Update libcurl to 8.10.1. --- subprojects/libcurl.wrap | 12 +- subprojects/packagefiles/curl/meson.build | 232 ++++++++++++++++++ .../packagefiles/curl/meson_options.txt | 1 + 3 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 subprojects/packagefiles/curl/meson.build create mode 100644 subprojects/packagefiles/curl/meson_options.txt diff --git a/subprojects/libcurl.wrap b/subprojects/libcurl.wrap index df7c7745c..ce2622169 100644 --- a/subprojects/libcurl.wrap +++ b/subprojects/libcurl.wrap @@ -1,11 +1,9 @@ [wrap-file] -directory = curl-8.9.1 -source_url = https://curl.se/download/curl-8.9.1.tar.xz -source_filename = curl-8.9.1.tar.xz -source_hash = f292f6cc051d5bbabf725ef85d432dfeacc8711dd717ea97612ae590643801e5 -patch_url = https://skuller.net/meson/libcurl_8.9.1-1_patch.zip -patch_filename = libcurl_8.9.1-1_patch.zip -patch_hash = b7f4ef775d912da335387b222dc1caf421fb434cc36aeee84a032fb28cac5c77 +directory = curl-8.10.1 +source_url = https://curl.se/download/curl-8.10.1.tar.xz +source_filename = curl-8.10.1.tar.xz +source_hash = 73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee +patch_directory = curl [provide] libcurl = libcurl_dep diff --git a/subprojects/packagefiles/curl/meson.build b/subprojects/packagefiles/curl/meson.build new file mode 100644 index 000000000..919fd9c99 --- /dev/null +++ b/subprojects/packagefiles/curl/meson.build @@ -0,0 +1,232 @@ +project('libcurl', 'c', version: '8.10.1', license: 'bsd') + +src = [ + 'lib/vauth/cleartext.c', + 'lib/vauth/cram.c', + 'lib/vauth/digest.c', + 'lib/vauth/digest_sspi.c', + 'lib/vauth/gsasl.c', + 'lib/vauth/krb5_gssapi.c', + 'lib/vauth/krb5_sspi.c', + 'lib/vauth/ntlm.c', + 'lib/vauth/ntlm_sspi.c', + 'lib/vauth/oauth2.c', + 'lib/vauth/spnego_gssapi.c', + 'lib/vauth/spnego_sspi.c', + 'lib/vauth/vauth.c', + + 'lib/vtls/bearssl.c', + 'lib/vtls/gtls.c', + 'lib/vtls/hostcheck.c', + 'lib/vtls/keylog.c', + 'lib/vtls/mbedtls.c', + 'lib/vtls/mbedtls_threadlock.c', + 'lib/vtls/openssl.c', + 'lib/vtls/rustls.c', + 'lib/vtls/schannel.c', + 'lib/vtls/schannel_verify.c', + 'lib/vtls/sectransp.c', + 'lib/vtls/vtls.c', + 'lib/vtls/wolfssl.c', + 'lib/vtls/x509asn1.c', + + 'lib/vquic/curl_msh3.c', + 'lib/vquic/curl_ngtcp2.c', + 'lib/vquic/curl_osslq.c', + 'lib/vquic/curl_quiche.c', + 'lib/vquic/vquic.c', + 'lib/vquic/vquic-tls.c', + + 'lib/vssh/libssh.c', + 'lib/vssh/libssh2.c', + 'lib/vssh/wolfssh.c', + + 'lib/altsvc.c', + 'lib/amigaos.c', + 'lib/asyn-ares.c', + 'lib/asyn-thread.c', + 'lib/base64.c', + 'lib/bufq.c', + 'lib/bufref.c', + 'lib/c-hyper.c', + 'lib/cf-h1-proxy.c', + 'lib/cf-h2-proxy.c', + 'lib/cf-haproxy.c', + 'lib/cf-https-connect.c', + 'lib/cf-socket.c', + 'lib/cfilters.c', + 'lib/conncache.c', + 'lib/connect.c', + 'lib/content_encoding.c', + 'lib/cookie.c', + 'lib/curl_addrinfo.c', + 'lib/curl_des.c', + 'lib/curl_endian.c', + 'lib/curl_fnmatch.c', + 'lib/curl_get_line.c', + 'lib/curl_gethostname.c', + 'lib/curl_gssapi.c', + 'lib/curl_memrchr.c', + 'lib/curl_multibyte.c', + 'lib/curl_ntlm_core.c', + 'lib/curl_path.c', + 'lib/curl_range.c', + 'lib/curl_rtmp.c', + 'lib/curl_sasl.c', + 'lib/curl_sha512_256.c', + 'lib/curl_sspi.c', + 'lib/curl_threads.c', + 'lib/curl_trc.c', + 'lib/cw-out.c', + 'lib/dict.c', + 'lib/dllmain.c', + 'lib/doh.c', + 'lib/dynbuf.c', + 'lib/dynhds.c', + 'lib/easy.c', + 'lib/easygetopt.c', + 'lib/easyoptions.c', + 'lib/escape.c', + 'lib/file.c', + 'lib/fileinfo.c', + 'lib/fopen.c', + 'lib/formdata.c', + 'lib/ftp.c', + 'lib/ftplistparser.c', + 'lib/getenv.c', + 'lib/getinfo.c', + 'lib/gopher.c', + 'lib/hash.c', + 'lib/headers.c', + 'lib/hmac.c', + 'lib/hostasyn.c', + 'lib/hostip.c', + 'lib/hostip4.c', + 'lib/hostip6.c', + 'lib/hostsyn.c', + 'lib/hsts.c', + 'lib/http.c', + 'lib/http1.c', + 'lib/http2.c', + 'lib/http_aws_sigv4.c', + 'lib/http_chunks.c', + 'lib/http_digest.c', + 'lib/http_negotiate.c', + 'lib/http_ntlm.c', + 'lib/http_proxy.c', + 'lib/idn.c', + 'lib/if2ip.c', + 'lib/imap.c', + 'lib/inet_ntop.c', + 'lib/inet_pton.c', + 'lib/krb5.c', + 'lib/ldap.c', + 'lib/llist.c', + 'lib/macos.c', + 'lib/md4.c', + 'lib/md5.c', + 'lib/memdebug.c', + 'lib/mime.c', + 'lib/mprintf.c', + 'lib/mqtt.c', + 'lib/multi.c', + 'lib/netrc.c', + 'lib/nonblock.c', + 'lib/noproxy.c', + 'lib/openldap.c', + 'lib/parsedate.c', + 'lib/pingpong.c', + 'lib/pop3.c', + 'lib/progress.c', + 'lib/psl.c', + 'lib/rand.c', + 'lib/rename.c', + 'lib/request.c', + 'lib/rtsp.c', + 'lib/select.c', + 'lib/sendf.c', + 'lib/setopt.c', + 'lib/sha256.c', + 'lib/share.c', + 'lib/slist.c', + 'lib/smb.c', + 'lib/smtp.c', + 'lib/socketpair.c', + 'lib/socks.c', + 'lib/socks_gssapi.c', + 'lib/socks_sspi.c', + 'lib/speedcheck.c', + 'lib/splay.c', + 'lib/strcase.c', + 'lib/strdup.c', + 'lib/strerror.c', + 'lib/strtok.c', + 'lib/strtoofft.c', + 'lib/system_win32.c', + 'lib/telnet.c', + 'lib/tftp.c', + 'lib/timediff.c', + 'lib/timeval.c', + 'lib/transfer.c', + 'lib/url.c', + 'lib/urlapi.c', + 'lib/version.c', + 'lib/version_win32.c', + 'lib/warnless.c', + 'lib/ws.c', +] + +if host_machine.system() != 'windows' + error('Only Windows supported') +endif + +cc = meson.get_compiler('c') + +args = [ + '-DBUILDING_LIBCURL', + '-DUSE_SCHANNEL', + '-DUSE_WINDOWS_SSPI', + '-DUSE_IPV6', + '-DCURL_STATICLIB', + '-DHTTP_ONLY', + '-DCURL_DISABLE_CRYPTO_AUTH', +] + +if get_option('windows-xp-compat') + args += '-D_WIN32_WINNT=0x0501' +else + args += '-D_WIN32_WINNT=0x0601' +endif + +zlib = dependency('zlib', required: false) +if zlib.found() + args += '-DHAVE_LIBZ' +endif + +deps = [ + cc.find_library('advapi32'), + cc.find_library('bcrypt'), + cc.find_library('crypt32'), + cc.find_library('ws2_32'), + zlib, +] + +nghttp2 = dependency('libnghttp2', required: false) +if nghttp2.found() and cc.has_header_symbol('sspi.h', 'SecApplicationProtocolNegotiationExt_ALPN', + prefix: '#include ', args: ['-DSECURITY_WIN32=1']) + args += ['-DUSE_NGHTTP2', '-DHAS_ALPN=1'] + deps += nghttp2 +endif + +libcurl = static_library('curl', src, + c_args: args, + dependencies: deps, + include_directories: ['include', 'lib'], +) + +libcurl_dep = declare_dependency( + compile_args: ['-DCURL_STATICLIB'], + dependencies: deps, + include_directories: 'include', + link_with: libcurl, +) diff --git a/subprojects/packagefiles/curl/meson_options.txt b/subprojects/packagefiles/curl/meson_options.txt new file mode 100644 index 000000000..0d5cff144 --- /dev/null +++ b/subprojects/packagefiles/curl/meson_options.txt @@ -0,0 +1 @@ +option('windows-xp-compat', type: 'boolean', value: false, yield: true) From 7c0b87da139dbea2a40d5c5f04f83fe1eadf9c3a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 19/43] Update openal-soft to 1.23.1. --- subprojects/openal-soft.wrap | 13 ++++++------- subprojects/packagefiles/openal-soft/meson.build | 3 +++ 2 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 subprojects/packagefiles/openal-soft/meson.build diff --git a/subprojects/openal-soft.wrap b/subprojects/openal-soft.wrap index 8292a54cd..8701045bb 100644 --- a/subprojects/openal-soft.wrap +++ b/subprojects/openal-soft.wrap @@ -1,11 +1,10 @@ [wrap-file] -directory = openal-soft-1.22.2 -source_url = https://openal-soft.org/openal-releases/openal-soft-1.22.2.tar.bz2 -source_filename = openal-soft-1.22.2.tar.bz2 -source_hash = ae94cc95cda76b7cc6e92e38c2531af82148e76d3d88ce996e2928a1ea7c3d20 -patch_url = https://skuller.net/meson/openal-soft_1.22.2-1_patch.zip -patch_filename = openal-soft_1.22.2-1_patch.zip -patch_hash = 8f0df33a04aec9fbac053476a9d68fd201bc74ed6ba62fce00138b71eb583db0 +directory = openal-soft-1.23.1 +source_url = https://github.com/kcat/openal-soft/releases/download/1.23.1/openal-soft-1.23.1.tar.bz2 +source_fallback_url = https://openal-soft.org/openal-releases/openal-soft-1.23.1.tar.bz2 +source_filename = openal-soft-1.23.1.tar.bz2 +source_hash = 796f4b89134c4e57270b7f0d755f0fa3435b90da437b745160a49bd41c845b21 +patch_directory = openal-soft [provide] openal = openal_dep diff --git a/subprojects/packagefiles/openal-soft/meson.build b/subprojects/packagefiles/openal-soft/meson.build new file mode 100644 index 000000000..e33717f57 --- /dev/null +++ b/subprojects/packagefiles/openal-soft/meson.build @@ -0,0 +1,3 @@ +project('openal-soft', version: '1.23.1') + +openal_dep = declare_dependency(include_directories: 'include') From 013c8fb7e31b8f412c867ec5f47aad767a4272ce Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 20/43] Update khr-headers to 20240924. --- subprojects/khr-headers.wrap | 6 ++---- subprojects/packagefiles/khr-headers.tar.xz | Bin 0 -> 111740 bytes 2 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 subprojects/packagefiles/khr-headers.tar.xz diff --git a/subprojects/khr-headers.wrap b/subprojects/khr-headers.wrap index 3e3caa844..cedbb6bd9 100644 --- a/subprojects/khr-headers.wrap +++ b/subprojects/khr-headers.wrap @@ -1,5 +1,3 @@ [wrap-file] -directory = khr-headers-20220530 -source_url = https://skuller.net/meson/khr-headers-20220530.zip -source_filename = khr-headers-20220530.zip -source_hash = c90019de9f55afae8e6a8b15ae4e23d921b98c1571af9fcfd5525df059a160f3 +directory = khr-headers +source_filename = khr-headers.tar.xz diff --git a/subprojects/packagefiles/khr-headers.tar.xz b/subprojects/packagefiles/khr-headers.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..98bb44528dad5726d9329fce969c086ff54c39d0 GIT binary patch literal 111740 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 literal 0 HcmV?d00001 From ffec154e778a77fcc27d91fc76f807255a672750 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 21/43] Unignore subprojects/packagefiles. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4d3a07c8a..300eac43f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /subprojects/*/ +!/subprojects/packagefiles/ /tags /TAGS From f15a750acfee1640b6f05bd07e4e1a7bca2ac294 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 22:45:30 +0300 Subject: [PATCH 22/43] Update README.md. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 4f87a80af..e4c4e4089 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,5 @@ For building Q2PRO, consult the INSTALL.md file. Q2PRO doesn't have releases. It is always recommended to use the git master version. -Nightly Windows builds are available at https://skuller.net/q2pro/ - For information on using and configuring Q2PRO, refer to client and server manuals available in doc/ subdirectory. From 27df08e00428c87255efc3e39b3e72b7303192fc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 26 Sep 2024 16:19:26 +0300 Subject: [PATCH 23/43] Silence compiler warning. --- src/server/mvd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/mvd.c b/src/server/mvd.c index b547035ee..a4eaa8952 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -1106,7 +1106,7 @@ SV_MvdMulticast */ void SV_MvdMulticast(const mleaf_t *leaf, multicast_t to, bool reliable) { - int leafnum; + int leafnum = 0; mvd_ops_t op; sizebuf_t *buf; From 287ef4608ee7567e0fd3c9a89f5ae71141d67739 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 26 Sep 2024 00:48:35 +0300 Subject: [PATCH 24/43] Add CI workflow for nightly builds. --- .ci/configure-ffmpeg.sh | 76 ++++++++++++++++++ .ci/i686-w64-mingw32.txt | 1 + .ci/nightly.sh | 145 ++++++++++++++++++++++++++++++++++ .ci/readme-template-rr.txt | 13 +++ .ci/readme-template.txt | 10 +++ .ci/x86_64-w64-mingw32.txt | 1 + .github/workflows/nightly.yml | 48 +++++++++++ 7 files changed, 294 insertions(+) create mode 100755 .ci/configure-ffmpeg.sh create mode 100755 .ci/nightly.sh create mode 100644 .ci/readme-template-rr.txt create mode 100644 .ci/readme-template.txt create mode 100644 .github/workflows/nightly.yml diff --git a/.ci/configure-ffmpeg.sh b/.ci/configure-ffmpeg.sh new file mode 100755 index 000000000..ee81dfab9 --- /dev/null +++ b/.ci/configure-ffmpeg.sh @@ -0,0 +1,76 @@ +#!/bin/sh -e + +OPTS_COMMON="--disable-everything \ + --enable-decoder=theora \ + --enable-decoder=vorbis \ + --enable-decoder=idcin \ + --enable-decoder=pcm_* \ + --disable-decoder=pcm_bluray \ + --disable-decoder=pcm_dvd \ + --disable-decoder=pcm_alaw_at \ + --disable-decoder=pcm_mulaw_at \ + --enable-demuxer=ogg \ + --enable-demuxer=idcin \ + --enable-demuxer=wav \ + --enable-parser=vp3 \ + --enable-parser=vorbis \ + --disable-protocols \ + --enable-protocol=file \ + --disable-avdevice \ + --disable-avfilter \ + --disable-postproc \ + --disable-programs \ + --disable-autodetect \ + --disable-network \ + --disable-doc \ + --disable-swscale-alpha \ + --enable-small \ + --disable-pthreads \ + --disable-w32threads" + +config_linux() { + ../configure --prefix="$1" $OPTS_COMMON +} + +config_win32() { + ../configure \ + --prefix="$1" \ + --cross-prefix=i686-w64-mingw32- \ + --arch=x86 \ + --target-os=mingw32 \ + --extra-cflags='-msse2 -mfpmath=sse' \ + $OPTS_COMMON +} + +config_win64() { + ../configure \ + --prefix="$1" \ + --cross-prefix=x86_64-w64-mingw32- \ + --arch=x86 \ + --target-os=mingw64 \ + $OPTS_COMMON +} + +usage() { + echo "Usage: $0 " + exit 1 +} + +if [ -z "$2" ] ; then + usage +fi + +case "$1" in + --win32) + config_win32 "$2" + ;; + --win64) + config_win64 "$2" + ;; + --linux) + config_linux "$2" + ;; + *) + usage + ;; +esac diff --git a/.ci/i686-w64-mingw32.txt b/.ci/i686-w64-mingw32.txt index 783095616..a28437cd5 100644 --- a/.ci/i686-w64-mingw32.txt +++ b/.ci/i686-w64-mingw32.txt @@ -5,6 +5,7 @@ ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' windres = 'i686-w64-mingw32-windres' nasm = 'nasm' +pkg-config = 'pkg-config' [host_machine] system = 'windows' diff --git a/.ci/nightly.sh b/.ci/nightly.sh new file mode 100755 index 000000000..a32f49e46 --- /dev/null +++ b/.ci/nightly.sh @@ -0,0 +1,145 @@ +#!/bin/sh -ex + +MESON_OPTS_COMMON="--auto-features=enabled --fatal-meson-warnings \ + -Dwerror=true -Dwrap_mode=forcefallback" + +MESON_OPTS="$MESON_OPTS_COMMON \ + -Dgame-build-options=optimization=s,b_lto=true \ + -Dsdl2=disabled -Dwayland=disabled -Dx11=disabled" + +SRC_DIR=`pwd` +CI=$SRC_DIR/.ci + +TMP_DIR=$SRC_DIR/q2pro-build +mkdir $TMP_DIR + +export MESON_PACKAGE_CACHE_DIR=$SRC_DIR/subprojects/packagecache + +### Source ### + +REV=$(git rev-list --count HEAD) +SHA=$(git rev-parse --short HEAD) +VER="r$REV~$SHA" +SRC="q2pro-r$REV" + +cd $TMP_DIR +GIT_DIR=$SRC_DIR/.git git archive --format=tar --prefix=$SRC/ HEAD | tar x +echo "$VER" > $SRC/VERSION +rm -rf $SRC/.gitignore $SRC/.ci $SRC/.github +fakeroot tar czf q2pro-source.tar.gz $SRC + +sed -e "s/##VER##/$VER/" -e "s/##DATE##/`date -R`/" $CI/readme-template.txt > README +sed -e "s/##VER##/$VER/" -e "s/##DATE##/`date -R`/" $CI/readme-template-rr.txt > README.rr + +### FFmpeg ### + +cd $TMP_DIR +git clone --depth=1 https://github.com/FFmpeg/FFmpeg.git ffmpeg +cd ffmpeg + +mkdir build-mingw-32 +cd build-mingw-32 +$CI/configure-ffmpeg.sh --win32 $TMP_DIR/ffmpeg-prefix-32 +make -j4 install +cd .. + +mkdir build-mingw-64 +cd build-mingw-64 +$CI/configure-ffmpeg.sh --win64 $TMP_DIR/ffmpeg-prefix-64 +make -j4 install +cd .. + +### Win32 ### + +export PKG_CONFIG_SYSROOT_DIR="$TMP_DIR/ffmpeg-prefix-32" +export PKG_CONFIG_LIBDIR="$PKG_CONFIG_SYSROOT_DIR/lib/pkgconfig" + +cd $TMP_DIR +meson setup --cross-file $CI/i686-w64-mingw32.txt $MESON_OPTS build-mingw-32 $SRC +cd build-mingw-32 +ninja +i686-w64-mingw32-strip q2pro.exe q2proded.exe gamex86.dll + +unix2dos -k -n ../$SRC/LICENSE LICENSE.txt ../$SRC/doc/client.asciidoc MANUAL.txt ../README README.txt +mkdir baseq2 +cp -a ../$SRC/src/client/ui/q2pro.menu baseq2/ +mv gamex86.dll baseq2/ + +zip -9 ../q2pro-client_win32_x86.zip \ + q2pro.exe \ + LICENSE.txt \ + MANUAL.txt \ + README.txt \ + baseq2/q2pro.menu \ + baseq2/gamex86.dll + +unix2dos -k -n ../$SRC/doc/server.asciidoc MANUAL.txt +zip -9 ../q2pro-server_win32_x86.zip \ + q2proded.exe \ + LICENSE.txt \ + MANUAL.txt \ + README.txt + +### Win64 ### + +export PKG_CONFIG_SYSROOT_DIR="$TMP_DIR/ffmpeg-prefix-64" +export PKG_CONFIG_LIBDIR="$PKG_CONFIG_SYSROOT_DIR/lib/pkgconfig" + +cd $TMP_DIR +meson setup --cross-file $CI/x86_64-w64-mingw32.txt $MESON_OPTS build-mingw-64 $SRC +cd build-mingw-64 +ninja +x86_64-w64-mingw32-strip q2pro.exe q2proded.exe gamex86_64.dll + +unix2dos -k -n ../$SRC/LICENSE LICENSE.txt ../$SRC/doc/client.asciidoc MANUAL.txt ../README README.txt +mkdir baseq2 +cp -a ../$SRC/src/client/ui/q2pro.menu baseq2/ +mv gamex86_64.dll baseq2/ +mv q2pro.exe q2pro64.exe +mv q2proded.exe q2proded64.exe + +zip -9 ../q2pro-client_win64_x64.zip \ + q2pro64.exe \ + LICENSE.txt \ + MANUAL.txt \ + README.txt \ + baseq2/q2pro.menu \ + baseq2/gamex86_64.dll + +unix2dos -k -n ../$SRC/doc/server.asciidoc MANUAL.txt +zip -9 ../q2pro-server_win64_x64.zip \ + q2proded64.exe \ + LICENSE.txt \ + MANUAL.txt \ + README.txt + +### Win64-rerelease ### + +cd $TMP_DIR +git clone https://github.com/skullernet/q2pro-rerelease-dll.git +cd q2pro-rerelease-dll +meson setup --cross-file $CI/x86_64-w64-mingw32.txt $MESON_OPTS_COMMON build-mingw +ninja -C build-mingw +x86_64-w64-mingw32-strip build-mingw/gamex86_64.dll +cd etc +zip -9 ../build-mingw/q2pro.pkz default.cfg q2pro.menu + +cd $TMP_DIR/build-mingw-64 + +mv q2pro64.exe q2pro.exe +cp -a ../q2pro-rerelease-dll/build-mingw/q2pro.pkz baseq2/ +cp -a ../q2pro-rerelease-dll/build-mingw/gamex86_64.dll baseq2/ +unix2dos -k -n ../$SRC/doc/client.asciidoc MANUAL.txt ../README.rr README.txt + +zip -9 ../q2pro-rerelease-client_win64_x64.zip \ + q2pro.exe \ + LICENSE.txt \ + MANUAL.txt \ + README.txt \ + baseq2/q2pro.pkz \ + baseq2/gamex86_64.dll + +### Version ### + +cd $TMP_DIR +echo $VER > version.txt diff --git a/.ci/readme-template-rr.txt b/.ci/readme-template-rr.txt new file mode 100644 index 000000000..ac6488334 --- /dev/null +++ b/.ci/readme-template-rr.txt @@ -0,0 +1,13 @@ +Welcome to Q2PRO, an enhanced Quake 2 client and server. + +Version ##VER## has been automatically built ##DATE## +from git master branch. + +Installation: extract this archive into new directory, then create shortcut to +q2pro.exe with the following command line: + + q2pro.exe +set basedir "" +set homedir "." + +For information on using and configuring Q2PRO, refer to MANUAL file. + +Project homepage: https://github.com/skullernet/q2pro diff --git a/.ci/readme-template.txt b/.ci/readme-template.txt new file mode 100644 index 000000000..57be9c249 --- /dev/null +++ b/.ci/readme-template.txt @@ -0,0 +1,10 @@ +Welcome to Q2PRO, an enhanced Quake 2 client and server. + +Version ##VER## has been automatically built ##DATE## +from git master branch. + +Installation: extract this archive into your Quake 2 directory. + +For information on using and configuring Q2PRO, refer to MANUAL file. + +Project homepage: https://github.com/skullernet/q2pro diff --git a/.ci/x86_64-w64-mingw32.txt b/.ci/x86_64-w64-mingw32.txt index c61705200..8a2b90c4a 100644 --- a/.ci/x86_64-w64-mingw32.txt +++ b/.ci/x86_64-w64-mingw32.txt @@ -5,6 +5,7 @@ ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' windres = 'x86_64-w64-mingw32-windres' nasm = 'nasm' +pkg-config = 'pkg-config' [host_machine] system = 'windows' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..e0c6988b8 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,48 @@ +name: nightly + +on: + push: + branches: [master] + +permissions: + contents: write + +jobs: + mingw: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/cache@v4 + with: + path: subprojects/packagecache + key: ${{ hashFiles('subprojects/*.wrap') }} + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y gcc-mingw-w64 dos2unix nasm meson ninja-build + + - name: Build + run: ./.ci/nightly.sh + + - name: Update release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release edit nightly -t "Nightly" -n "Latest nightly build `cat q2pro-build/version.txt`." --latest + gh release upload nightly q2pro-build/*.zip q2pro-build/*.tar.gz q2pro-build/version.txt --clobber + + - name: Update tag + uses: actions/github-script@v7 + with: + script: | + github.rest.git.updateRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: "tags/nightly", + sha: context.sha, + force: true + }) From 47ef036b4580b3d595b39b3346f6105a23405eef Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 27 Sep 2024 18:38:53 +0300 Subject: [PATCH 25/43] Use enum instead of magic constant. --- src/refresh/tess.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index dd6380d37..3d775dc67 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -658,7 +658,7 @@ void GL_Flush3D(void) GL_ArrayBits(array); if (state & GLS_DEFAULT_SKY) { - GL_BindCubemap(tess.texnum[0]); + GL_BindCubemap(tess.texnum[TMU_TEXTURE]); } else if (qglBindTextures) { #if USE_DEBUG if (q_unlikely(gl_nobind->integer)) From 014160f20cf70931e2f6cd44670ccdab5f315042 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 27 Sep 2024 18:52:50 +0300 Subject: [PATCH 26/43] Warn if DECOUPLED_LM lump has invalid offsets. --- src/common/bsp.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index 4f93f3344..51ebe84ed 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -404,6 +404,7 @@ int BSP_LoadMaterials(bsp_t *bsp) static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) { mface_t *out; + bool errors; if (filelen % DECOUPLED_LM_BYTES) { Com_WPrintf("DECOUPLED_LM lump has odd size\n"); @@ -416,15 +417,20 @@ static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) } out = bsp->faces; + errors = false; for (int i = 0; i < bsp->numfaces; i++, out++) { out->lm_width = BSP_Short(); out->lm_height = BSP_Short(); uint32_t offset = BSP_Long(); - if (offset < bsp->numlightmapbytes) + if (offset == -1) + out->lightmap = NULL; + else if (offset < bsp->numlightmapbytes) out->lightmap = bsp->lightmap + offset; - else + else { out->lightmap = NULL; + errors = true; + } for (int j = 0; j < 2; j++) { BSP_Vector(out->lm_axis[j]); @@ -432,6 +438,9 @@ static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) } } + if (errors) + Com_WPrintf("DECOUPLED_LM lump possibly corrupted\n"); + bsp->lm_decoupled = true; } From 1a4ae732e348e426c22f101bbef535f8347918db Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 1 Oct 2024 21:22:33 +0300 Subject: [PATCH 27/43] Add COM_ParseToken(). Allow parsing into user supplied buffer. Also add line number counter. --- inc/shared/shared.h | 4 ++++ src/client/ascii.c | 4 ++-- src/client/main.c | 2 +- src/client/screen.c | 12 +++++------ src/shared/shared.c | 51 +++++++++++++++++++++++++++++++-------------- 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index c77e00e7d..abf69c6d7 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -594,6 +594,10 @@ bool COM_IsUint(const char *s); bool COM_IsPath(const char *s); bool COM_IsWhite(const char *s); +extern unsigned com_linenum; + +#define COM_SkipToken(data_p) COM_ParseToken(data_p, NULL, 0) +size_t COM_ParseToken(const char **data_p, char *buffer, size_t size); char *COM_Parse(const char **data_p); // data is an in/out parm, returns a parsed out token size_t COM_Compress(char *data); diff --git a/src/client/ascii.c b/src/client/ascii.c index dce044930..8070ac700 100644 --- a/src/client/ascii.c +++ b/src/client/ascii.c @@ -151,7 +151,7 @@ static void TH_DrawLayoutString(char *dst, const char *s) if (!strcmp(token, "pic")) { // draw a pic from a stat number - COM_Parse(&s); + COM_SkipToken(&s); continue; } @@ -223,7 +223,7 @@ static void TH_DrawLayoutString(char *dst, const char *s) if (!strcmp(token, "picn")) { // draw a pic from a name - COM_Parse(&s); + COM_SkipToken(&s); continue; } diff --git a/src/client/main.c b/src/client/main.c index cf1946658..5f4b6e88a 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -858,7 +858,7 @@ static void CL_ParseStatusResponse(serverStatus_t *status, const char *string) player = &status->players[status->numPlayers]; player->score = Q_atoi(COM_Parse(&s)); player->ping = Q_atoi(COM_Parse(&s)); - Q_strlcpy(player->name, COM_Parse(&s), sizeof(player->name)); + COM_ParseToken(&s, player->name, sizeof(player->name)); if (!s) break; status->numPlayers++; diff --git a/src/client/screen.c b/src/client/screen.c index 06ebd08e1..1a5a895b3 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1605,25 +1605,25 @@ static void SCR_SkipToEndif(const char **s) !strcmp(token, "yt") || !strcmp(token, "yb") || !strcmp(token, "yv") || !strcmp(token, "pic") || !strcmp(token, "picn") || !strcmp(token, "color") || strstr(token, "string")) { - COM_Parse(s); + COM_SkipToken(s); continue; } if (!strcmp(token, "client")) { for (i = 0; i < 6; i++) - COM_Parse(s); + COM_SkipToken(s); continue; } if (!strcmp(token, "ctf")) { for (i = 0; i < 5; i++) - COM_Parse(s); + COM_SkipToken(s); continue; } if (!strcmp(token, "num") || !strcmp(token, "health_bars")) { - COM_Parse(s); - COM_Parse(s); + COM_SkipToken(s); + COM_SkipToken(s); continue; } @@ -1632,7 +1632,7 @@ static void SCR_SkipToEndif(const char **s) if (!strcmp(token, "rnum")) continue; if (!strcmp(token, "if")) { - COM_Parse(s); + COM_SkipToken(s); skip++; continue; } diff --git a/src/shared/shared.c b/src/shared/shared.c index cb9f8b6a7..cf6423340 100644 --- a/src/shared/shared.c +++ b/src/shared/shared.c @@ -413,8 +413,7 @@ char *vtos(const vec3_t v) return str[index]; } -static char com_token[4][MAX_TOKEN_CHARS]; -static int com_tokidx; +unsigned com_linenum; /* ============== @@ -424,22 +423,20 @@ Parse a token out of a string. Handles C and C++ comments. ============== */ -char *COM_Parse(const char **data_p) +size_t COM_ParseToken(const char **data_p, char *buffer, size_t size) { int c; - int len; + size_t len; const char *data; - char *s = com_token[com_tokidx]; - - com_tokidx = (com_tokidx + 1) & 3; data = *data_p; len = 0; - s[0] = 0; + if (size) + *buffer = 0; if (!data) { *data_p = NULL; - return s; + return len; } // skip whitespace @@ -447,7 +444,10 @@ char *COM_Parse(const char **data_p) while ((c = *data) <= ' ') { if (c == 0) { *data_p = NULL; - return s; + return len; + } + if (c == '\n') { + com_linenum++; } data++; } @@ -468,6 +468,9 @@ char *COM_Parse(const char **data_p) data += 2; break; } + if (data[0] == '\n') { + com_linenum++; + } data++; } goto skipwhite; @@ -481,26 +484,42 @@ char *COM_Parse(const char **data_p) if (c == '\"' || !c) { goto finish; } - - if (len < MAX_TOKEN_CHARS - 1) { - s[len++] = c; + if (c == '\n') { + com_linenum++; } + if (len + 1 < size) { + *buffer++ = c; + } + len++; } } // parse a regular word do { - if (len < MAX_TOKEN_CHARS - 1) { - s[len++] = c; + if (len + 1 < size) { + *buffer++ = c; } + len++; data++; c = *data; } while (c > 32); finish: - s[len] = 0; + if (size) + *buffer = 0; *data_p = data; + return len; +} + +char *COM_Parse(const char **data_p) +{ + static char com_token[4][MAX_TOKEN_CHARS]; + static int com_tokidx; + char *s = com_token[com_tokidx]; + + COM_ParseToken(data_p, s, sizeof(com_token[0])); + com_tokidx = (com_tokidx + 1) & 3; return s; } From cd30ec4bc8c6ddb68246395e630935b9d4dd97a4 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:00:54 +0300 Subject: [PATCH 28/43] Use real JSON parser to parse MD5 scales. --- src/refresh/jsmn.h | 470 +++++++++++++++++++++++++++++++++++++++++++ src/refresh/models.c | 98 +++++---- 2 files changed, 530 insertions(+), 38 deletions(-) create mode 100644 src/refresh/jsmn.h diff --git a/src/refresh/jsmn.h b/src/refresh/jsmn.h new file mode 100644 index 000000000..88570f102 --- /dev/null +++ b/src/refresh/jsmn.h @@ -0,0 +1,470 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef JSMN_H +#define JSMN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct jsmntok { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct jsmn_parser { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing a single JSON object. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/** + * Allocates a fresh unused token from the token pool. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type, + const int start, const int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + /* to quiet a warning from gcc*/ + break; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + /* Skip starting quote */ + parser->pos++; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; + i++) { + /* If it isn't a hex character we have an error */ + if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': + case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + return JSMN_ERROR_NOMEM; + } + if (parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; +#ifdef JSMN_STRICT + /* In strict mode an object or array can't become a key */ + if (t->type == JSMN_OBJECT) { + return JSMN_ERROR_INVAL; + } +#endif + t->size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': + case ']': + if (tokens == NULL) { + break; + } + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if (token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) { + return JSMN_ERROR_INVAL; + } + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + case '\t': + case '\r': + case '\n': + case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 't': + case 'f': + case 'n': + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + const jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +JSMN_API void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */ diff --git a/src/refresh/models.c b/src/refresh/models.c index 8976fdd34..4841f1c23 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -703,6 +703,10 @@ static void MOD_PrintError(const char *path, int err) #if USE_MD5 +#define JSMN_STATIC +#define JSMN_PARENT_LINKS +#include "jsmn.h" + #define MD5_Malloc(size) Hunk_TryAlloc(&model->skeleton_hunk, size) static bool MD5_ParseExpect(const char **buffer, const char *expect) @@ -1085,69 +1089,87 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, */ static void MOD_LoadMD5Scale(md5_model_t *model, const char *path, joint_info_t *joint_infos) { - void *buffer; - const char *s; - int ret; + const jsmntok_t *tok, *end; + jsmn_parser parser; + jsmntok_t tokens[4096]; + char *data; + int len, ret; + + len = FS_LoadFile(path, (void **)&data); + if (!data) { + if (len != Q_ERR(ENOENT)) + MOD_PrintError(path, len); + return; + } - ret = FS_LoadFile(path, &buffer); - if (!buffer) + jsmn_init(&parser); + ret = jsmn_parse(&parser, data, len, tokens, q_countof(tokens)); + if (ret < 0) goto fail; - s = buffer; + if (ret == 0) + goto skip; - MD5_EXPECT("{"); - while (s) { - int joint_id = -1; - char *tok, *tok2; + tok = &tokens[0]; + if (tok->type != JSMN_OBJECT) + goto fail; - tok = COM_Parse(&s); - if (!strcmp(tok, "}")) - break; + end = tokens + ret; + tok++; + + while (tok < end) { + if (tok->type != JSMN_STRING) + goto fail; + int joint_id = -1; + const char *joint_name = data + tok->start; + + data[tok->end] = 0; for (int i = 0; i < model->num_joints; i++) { - if (!strcmp(tok, joint_infos[i].name)) { + if (!strcmp(joint_name, joint_infos[i].name)) { joint_id = i; break; } } if (joint_id == -1) - Com_WPrintf("No such joint %s in %s\n", Com_MakePrintable(tok), path); + Com_WPrintf("No such joint \"%s\" in %s\n", Com_MakePrintable(joint_name), path); - MD5_EXPECT(":"); - MD5_EXPECT("{"); + if (++tok == end || tok->type != JSMN_OBJECT) + goto fail; - while (s) { - tok = COM_Parse(&s); - if (!strcmp(tok, "}") || !strcmp(tok, "},")) - break; - MD5_EXPECT(":"); + int num_keys = tok->size; + if (end - ++tok < num_keys * 2) + goto fail; - tok2 = COM_Parse(&s); - if (joint_id == -1) - continue; + for (int i = 0; i < num_keys; i++) { + const jsmntok_t *key = tok++; + const jsmntok_t *val = tok++; + if (key->type != JSMN_STRING || val->type != JSMN_PRIMITIVE) + goto fail; - if (!strcmp(tok, "scale_positions")) { - joint_infos[joint_id].scale_pos = !strncmp(tok2, "true", 4); + if (joint_id == -1) continue; - } - unsigned frame_id = Q_atoi(tok); - if (frame_id >= model->num_frames) { - Com_WPrintf("No such frame %d in %s\n", frame_id, path); - continue; + data[key->end] = 0; + if (!strcmp(data + key->start, "scale_positions")) { + joint_infos[joint_id].scale_pos = data[val->start] == 't'; + } else { + unsigned frame_id = Q_atoi(data + key->start); + if (frame_id < model->num_frames) + model->skeleton_frames[frame_id * model->num_joints + joint_id].scale = Q_atof(data + val->start); + else + Com_WPrintf("No such frame %d in %s\n", frame_id, path); } - - model->skeleton_frames[frame_id * model->num_joints + joint_id].scale = Q_atof(tok2); } } - FS_FreeFile(buffer); +skip: + FS_FreeFile(data); return; fail: - if (ret != Q_ERR(ENOENT)) - MOD_PrintError(path, ret); - FS_FreeFile(buffer); + Com_EPrintf("Couldn't load %s: Invalid JSON data\n", path); + FS_FreeFile(data); } /** From da32d03e1943f13e2ab442757798013d50f2f008 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:00:54 +0300 Subject: [PATCH 29/43] Factor MD5 skin loading into separate function. --- src/refresh/models.c | 53 +++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/refresh/models.c b/src/refresh/models.c index 4841f1c23..9469dac6a 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1318,6 +1318,38 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) return false; } +static bool MD5_LoadSkins(model_t *model) +{ + md5_model_t *mdl = model->skeleton; + const maliasmesh_t *mesh = &model->meshes[0]; + + mdl->num_skins = mesh->numskins; + mdl->skins = Hunk_TryAlloc(&model->skeleton_hunk, sizeof(mdl->skins[0]) * mdl->num_skins); + if (!mdl->skins) { + Com_EPrintf("Out of memory for MD5 skins\n"); + return false; + } + + for (int i = 0; i < mesh->numskins; i++) { + // because skins are actually absolute and not always relative to the + // model being used, we have to stick to the same behavior. + char skin_name[MAX_QPATH], skin_path[MAX_QPATH]; + + COM_SplitPath(mesh->skinnames[i], skin_name, sizeof(skin_name), skin_path, sizeof(skin_path), false); + + // build MD5 path + if (Q_strlcat(skin_path, "md5/", sizeof(skin_path)) < sizeof(skin_path) && + Q_strlcat(skin_path, skin_name, sizeof(skin_path)) < sizeof(skin_path)) { + mdl->skins[i] = IMG_Find(skin_path, IT_SKIN, IF_NONE); + } else { + Com_WPrintf("MD5 skin path too long: %s\n", skin_path); + mdl->skins[i] = R_NOTEXTURE; + } + } + + return true; +} + static void MOD_LoadMD5(model_t *model) { char model_name[MAX_QPATH], base_path[MAX_QPATH]; @@ -1338,28 +1370,9 @@ static void MOD_LoadMD5(model_t *model) if (!MOD_LoadMD5Anim(model, anim_path)) goto fail; - - model->skeleton->num_skins = model->meshes[0].numskins; - if (!(model->skeleton->skins = MD5_Malloc(sizeof(model->skeleton->skins[0]) * model->meshes[0].numskins))) + if (!MD5_LoadSkins(model)) goto fail; - for (int i = 0; i < model->meshes[0].numskins; i++) { - // because skins are actually absolute and not always relative to the - // model being used, we have to stick to the same behavior. - char skin_name[MAX_QPATH], skin_path[MAX_QPATH]; - - COM_SplitPath(model->meshes[0].skinnames[i], skin_name, sizeof(skin_name), skin_path, sizeof(skin_path), false); - - // build md5 path - if (Q_strlcat(skin_path, "md5/", sizeof(skin_path)) < sizeof(skin_path) && - Q_strlcat(skin_path, skin_name, sizeof(skin_path)) < sizeof(skin_path)) { - model->skeleton->skins[i] = IMG_Find(skin_path, IT_SKIN, IF_NONE); - } else { - Com_WPrintf("MD5 skin path too long: %s\n", skin_path); - model->skeleton->skins[i] = R_NOTEXTURE; - } - } - Hunk_End(&model->skeleton_hunk); return; From b711e8f830d33b1767b05f82690b560423547253 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:00:54 +0300 Subject: [PATCH 30/43] Refactor MD5 code more. Use longjmp instead of macros for error handling. Get rid of md5_joint_t::parent. Get rid of md5_model_t::base_skeleton. Verify joint parent has been processed before use. Reduce size of md5_vertex_t and md5_weight_t to 16 bytes. Allow joint names up to 48 chars. --- src/refresh/gl.h | 16 +- src/refresh/mesh.c | 14 +- src/refresh/models.c | 438 +++++++++++++++++++++---------------------- 3 files changed, 221 insertions(+), 247 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index b0b4a8df3..920868ea1 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -320,34 +320,30 @@ typedef struct { // the total amount of joints the renderer will bother handling #define MD5_MAX_JOINTS 256 -#define MD5_MAX_JOINTNAME 32 +#define MD5_MAX_JOINTNAME 48 #define MD5_MAX_MESHES 32 #define MD5_MAX_WEIGHTS 8192 #define MD5_MAX_FRAMES 1024 /* Joint */ typedef struct { - int parent; - vec3_t pos; - quat_t orient; float scale; + quat_t orient; } md5_joint_t; /* Vertex */ typedef struct { vec3_t normal; - uint32_t start; /* start weight */ - uint32_t count; /* weight count */ + uint16_t start; /* start weight */ + uint16_t count; /* weight count */ } md5_vertex_t; /* Weight */ typedef struct { - int joint; - float bias; - vec3_t pos; + float bias; } md5_weight_t; /* Mesh */ @@ -360,6 +356,7 @@ typedef struct { maliastc_t *tcoords; glIndex_t *indices; md5_weight_t *weights; + uint8_t *jointnums; } md5_mesh_t; /* MD5 model + animation structure */ @@ -370,7 +367,6 @@ typedef struct { int num_skins; md5_mesh_t *meshes; - md5_joint_t *base_skeleton; md5_joint_t *skeleton_frames; // [num_joints][num_frames] image_t **skins; } md5_model_t; diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 64dd23f62..487c6f7cd 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -671,7 +671,7 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, // for the given vertex, set of weights & skeleton, calculate // the output vertex (and optionally normal). static inline void calc_skel_vert(const md5_vertex_t *vert, - const md5_weight_t *weights, + const md5_mesh_t *mesh, const md5_joint_t *skeleton, float *restrict out_position, float *restrict out_normal) @@ -682,8 +682,8 @@ static inline void calc_skel_vert(const md5_vertex_t *vert, VectorClear(out_normal); for (int i = 0; i < vert->count; i++) { - const md5_weight_t *weight = &weights[vert->start + i]; - const md5_joint_t *joint = &skeleton[weight->joint]; + const md5_weight_t *weight = &mesh->weights[vert->start + i]; + const md5_joint_t *joint = &skeleton[mesh->jointnums[vert->start + i]]; vec3_t wv; Quat_RotatePoint(joint->orient, weight->pos, wv); @@ -701,7 +701,7 @@ static inline void calc_skel_vert(const md5_vertex_t *vert, static void tess_plain_skel(const md5_mesh_t *mesh, const md5_joint_t *skeleton) { for (int i = 0; i < mesh->num_verts; i++) - calc_skel_vert(&mesh->vertices[i], mesh->weights, skeleton, &tess.vertices[i * 4], NULL); + calc_skel_vert(&mesh->vertices[i], mesh, skeleton, &tess.vertices[i * 4], NULL); } static void tess_shade_skel(const md5_mesh_t *mesh, const md5_joint_t *skeleton) @@ -710,7 +710,7 @@ static void tess_shade_skel(const md5_mesh_t *mesh, const md5_joint_t *skeleton) for (int i = 0; i < mesh->num_verts; i++) { vec3_t normal; - calc_skel_vert(&mesh->vertices[i], mesh->weights, skeleton, dst_vert, normal); + calc_skel_vert(&mesh->vertices[i], mesh, skeleton, dst_vert, normal); vec_t d = shadedot(normal); dst_vert[4] = color[0] * d; @@ -726,7 +726,7 @@ static void tess_shell_skel(const md5_mesh_t *mesh, const md5_joint_t *skeleton) { for (int i = 0; i < mesh->num_verts; i++) { vec3_t position, normal; - calc_skel_vert(&mesh->vertices[i], mesh->weights, skeleton, position, normal); + calc_skel_vert(&mesh->vertices[i], mesh, skeleton, position, normal); VectorMA(position, shellscale, normal, &tess.vertices[i * 4]); } @@ -740,9 +740,7 @@ static void lerp_alias_skeleton(const md5_model_t *model) const md5_joint_t *skel_b = &model->skeleton_frames[frame_b * model->num_joints]; for (int i = 0; i < model->num_joints; i++) { - temp_skeleton[i].parent = skel_a[i].parent; temp_skeleton[i].scale = skel_b[i].scale; - LerpVector2(skel_a[i].pos, skel_b[i].pos, backlerp, frontlerp, temp_skeleton[i].pos); Quat_SLerp(skel_a[i].orient, skel_b[i].orient, backlerp, frontlerp, temp_skeleton[i].orient); } diff --git a/src/refresh/models.c b/src/refresh/models.c index 9469dac6a..580d0dcf6 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -703,75 +703,94 @@ static void MOD_PrintError(const char *path, int err) #if USE_MD5 +#include + #define JSMN_STATIC #define JSMN_PARENT_LINKS #include "jsmn.h" -#define MD5_Malloc(size) Hunk_TryAlloc(&model->skeleton_hunk, size) +static jmp_buf md5_jmpbuf; -static bool MD5_ParseExpect(const char **buffer, const char *expect) +q_noreturn +static void MD5_ParseError(const char *text) { - char *token = COM_Parse(buffer); + Com_SetLastError(va("Line %u: %s", com_linenum, text)); + longjmp(md5_jmpbuf, -1); +} - if (strcmp(token, expect)) { - Com_SetLastError(va("Expected %s, got %s", expect, Com_MakePrintable(token))); - return false; +static void *MD5_Malloc(model_t *model, size_t size) +{ + void *ptr = Hunk_TryAlloc(&model->skeleton_hunk, size); + if (!ptr) { + Com_SetLastError("Out of memory"); + longjmp(md5_jmpbuf, -1); } + return ptr; +} - return true; +static void MD5_ParseExpect(const char **buffer, const char *expect) +{ + char *token = COM_Parse(buffer); + + if (strcmp(token, expect)) + MD5_ParseError(va("Expected \"%s\", got \"%s\"", expect, Com_MakePrintable(token))); } -static bool MD5_ParseFloat(const char **buffer, float *output) +static float MD5_ParseFloat(const char **buffer) { char *token = COM_Parse(buffer); char *endptr; - *output = strtof(token, &endptr); - if (endptr == token || *endptr) { - Com_SetLastError(va("Expected float, got %s", Com_MakePrintable(token))); - return false; - } + float v = strtof(token, &endptr); + if (endptr == token || *endptr) + MD5_ParseError(va("Expected float, got \"%s\"", Com_MakePrintable(token))); - return true; + return v; } -static bool MD5_ParseUint(const char **buffer, uint32_t *output) +static uint32_t MD5_ParseUint(const char **buffer, uint32_t min_v, uint32_t max_v) { char *token = COM_Parse(buffer); char *endptr; - *output = strtoul(token, &endptr, 10); - if (endptr == token || *endptr) { - Com_SetLastError(va("Expected int, got %s", Com_MakePrintable(token))); - return false; - } + unsigned long v = strtoul(token, &endptr, 10); + if (endptr == token || *endptr) + MD5_ParseError(va("Expected uint, got \"%s\"", Com_MakePrintable(token))); + if (v < min_v || v > max_v) + MD5_ParseError(va("Value out of range: %lu", v)); - return true; + return v; } -static bool MD5_ParseVector(const char **buffer, vec3_t output) +static int32_t MD5_ParseInt(const char **buffer, int32_t min_v, int32_t max_v) { - return - MD5_ParseExpect(buffer, "(") && - MD5_ParseFloat(buffer, &output[0]) && - MD5_ParseFloat(buffer, &output[1]) && - MD5_ParseFloat(buffer, &output[2]) && - MD5_ParseExpect(buffer, ")"); -} + char *token = COM_Parse(buffer); + char *endptr; + + long v = strtol(token, &endptr, 10); + if (endptr == token || *endptr) + MD5_ParseError(va("Expected int, got \"%s\"", Com_MakePrintable(token))); + if (v < min_v || v > max_v) + MD5_ParseError(va("Value out of range: %ld", v)); -#define MD5_CHECK(x) \ - do { if (!(x)) { ret = Q_ERR_INVALID_FORMAT; goto fail; } } while (0) + return v; +} -#define MD5_ENSURE(x, e) \ - do { if (!(x)) { Com_SetLastError(e); ret = Q_ERR_INVALID_FORMAT; goto fail; } } while (0) +static void MD5_ParseVector(const char **buffer, vec3_t output) +{ + MD5_ParseExpect(buffer, "("); + output[0] = MD5_ParseFloat(buffer); + output[1] = MD5_ParseFloat(buffer); + output[2] = MD5_ParseFloat(buffer); + MD5_ParseExpect(buffer, ")"); +} -#define MD5_EXPECT(x) MD5_CHECK(MD5_ParseExpect(&s, x)) -#define MD5_UINT(x) MD5_CHECK(MD5_ParseUint(&s, &x)) -#define MD5_FLOAT(x) MD5_CHECK(MD5_ParseFloat(&s, &x)) -#define MD5_VECTOR(x) MD5_CHECK(MD5_ParseVector(&s, x)) -#define MD5_SKIP() COM_Parse(&s) +typedef struct { + vec3_t pos; + quat_t orient; +} baseframe_joint_t; -static void MD5_ComputeNormals(md5_mesh_t *mesh, const md5_joint_t *base_skeleton) +static void MD5_ComputeNormals(md5_mesh_t *mesh, const baseframe_joint_t *base_skeleton) { vec3_t finalVerts[TESS_MAX_VERTICES]; md5_vertex_t *vert; @@ -786,7 +805,7 @@ static void MD5_ComputeNormals(md5_mesh_t *mesh, const md5_joint_t *base_skeleto for (j = 0; j < vert->count; j++) { const md5_weight_t *weight = &mesh->weights[vert->start + j]; - const md5_joint_t *joint = &base_skeleton[weight->joint]; + const baseframe_joint_t *joint = &base_skeleton[mesh->jointnums[vert->start + j]]; /* Calculate transformed vertex for this weight */ vec3_t wv; @@ -837,11 +856,12 @@ static void MD5_ComputeNormals(md5_mesh_t *mesh, const md5_joint_t *base_skeleto vec3_t *norm = HashMap_Lookup(vec3_t, pos_to_normal_map, &finalVerts[i]); if (norm) { // Put the bind-pose normal into joint-local space - // so the animated normal can be computed faster later - // Done by transforming the vertex normal by the inverse joint's orientation quaternion of the weight + // so the animated normal can be computed faster later. + // Done by transforming the vertex normal by the inverse + // joint's orientation quaternion of the weight. for (j = 0; j < vert->count; j++) { const md5_weight_t *weight = &mesh->weights[vert->start + j]; - const md5_joint_t *joint = &base_skeleton[weight->joint]; + const baseframe_joint_t *joint = &base_skeleton[mesh->jointnums[vert->start + j]]; vec3_t wv; quat_t orient_inv; Quat_Conjugate(joint->orient, orient_inv); @@ -854,182 +874,136 @@ static void MD5_ComputeNormals(md5_mesh_t *mesh, const md5_joint_t *base_skeleto HashMap_Destroy(pos_to_normal_map); } -static bool MOD_LoadMD5Mesh(model_t *model, const char *path) +static bool MD5_ParseMesh(model_t *model, const char *s, const char *path) { + baseframe_joint_t base_skeleton[MD5_MAX_JOINTS]; md5_model_t *mdl; - int i, j, k, ret; - uint32_t num_joints, num_meshes; - void *buffer; - const char *s; + int i, j, k; - ret = FS_LoadFile(path, &buffer); - if (!buffer) - goto fail; - s = buffer; + if (setjmp(md5_jmpbuf)) + return false; + + com_linenum = 1; // parse header - MD5_EXPECT("MD5Version"); - MD5_EXPECT("10"); + MD5_ParseExpect(&s, "MD5Version"); + MD5_ParseExpect(&s, "10"); // allocate data storage, now that we're definitely an MD5 Hunk_Begin(&model->skeleton_hunk, 0x800000); - OOM_CHECK(model->skeleton = mdl = MD5_Malloc(sizeof(*mdl))); + model->skeleton = mdl = MD5_Malloc(model, sizeof(*mdl)); - MD5_EXPECT("commandline"); - MD5_SKIP(); + MD5_ParseExpect(&s, "commandline"); + COM_SkipToken(&s); - MD5_EXPECT("numJoints"); - MD5_UINT(num_joints); - MD5_ENSURE(num_joints > 0, "No joints"); - MD5_ENSURE(num_joints <= MD5_MAX_JOINTS, "Too many joints"); - OOM_CHECK(mdl->base_skeleton = MD5_Malloc(num_joints * sizeof(mdl->base_skeleton[0]))); - mdl->num_joints = num_joints; + MD5_ParseExpect(&s, "numJoints"); + mdl->num_joints = MD5_ParseUint(&s, 1, MD5_MAX_JOINTS); - MD5_EXPECT("numMeshes"); - MD5_UINT(num_meshes); - MD5_ENSURE(num_meshes > 0, "No meshes"); - MD5_ENSURE(num_meshes <= MD5_MAX_MESHES, "Too many meshes"); - OOM_CHECK(mdl->meshes = MD5_Malloc(num_meshes * sizeof(mdl->meshes[0]))); - mdl->num_meshes = num_meshes; + MD5_ParseExpect(&s, "numMeshes"); + mdl->num_meshes = MD5_ParseUint(&s, 1, MD5_MAX_MESHES); - MD5_EXPECT("joints"); - MD5_EXPECT("{"); + MD5_ParseExpect(&s, "joints"); + MD5_ParseExpect(&s, "{"); - for (i = 0; i < num_joints; i++) { - md5_joint_t *joint = &mdl->base_skeleton[i]; + for (i = 0; i < mdl->num_joints; i++) { + baseframe_joint_t *joint = &base_skeleton[i]; // skip name - MD5_SKIP(); + COM_SkipToken(&s); - uint32_t parent; - MD5_UINT(parent); - MD5_ENSURE(parent == -1 || (parent < num_joints && parent != i), "Bad parent joint"); - joint->parent = parent; + // skip parent + COM_SkipToken(&s); - MD5_VECTOR(joint->pos); - MD5_VECTOR(joint->orient); + MD5_ParseVector(&s, joint->pos); + MD5_ParseVector(&s, joint->orient); Quat_ComputeW(joint->orient); - joint->scale = 1.0f; } - MD5_EXPECT("}"); + MD5_ParseExpect(&s, "}"); - for (i = 0; i < num_meshes; i++) { + mdl->meshes = MD5_Malloc(model, mdl->num_meshes * sizeof(mdl->meshes[0])); + for (i = 0; i < mdl->num_meshes; i++) { md5_mesh_t *mesh = &mdl->meshes[i]; - uint32_t num_verts, num_tris, num_weights; - MD5_EXPECT("mesh"); - MD5_EXPECT("{"); + MD5_ParseExpect(&s, "mesh"); + MD5_ParseExpect(&s, "{"); - MD5_EXPECT("shader"); - MD5_SKIP(); + MD5_ParseExpect(&s, "shader"); + COM_SkipToken(&s); - MD5_EXPECT("numverts"); - MD5_UINT(num_verts); - MD5_ENSURE(num_verts <= TESS_MAX_VERTICES, "Too many verts"); - OOM_CHECK(mesh->vertices = MD5_Malloc(num_verts * sizeof(mesh->vertices[0]))); - OOM_CHECK(mesh->tcoords = MD5_Malloc(num_verts * sizeof(mesh->tcoords[0]))); - mesh->num_verts = num_verts; + MD5_ParseExpect(&s, "numverts"); + mesh->num_verts = MD5_ParseUint(&s, 0, TESS_MAX_VERTICES); + mesh->vertices = MD5_Malloc(model, mesh->num_verts * sizeof(mesh->vertices[0])); + mesh->tcoords = MD5_Malloc(model, mesh->num_verts * sizeof(mesh->tcoords [0])); - for (j = 0; j < num_verts; j++) { - MD5_EXPECT("vert"); + for (j = 0; j < mesh->num_verts; j++) { + MD5_ParseExpect(&s, "vert"); - uint32_t vert_index; - MD5_UINT(vert_index); - MD5_ENSURE(vert_index < num_verts, "Bad vert index"); + uint32_t vert_index = MD5_ParseUint(&s, 0, mesh->num_verts - 1); maliastc_t *tc = &mesh->tcoords[vert_index]; - MD5_EXPECT("("); - MD5_FLOAT(tc->st[0]); - MD5_FLOAT(tc->st[1]); - MD5_EXPECT(")"); + MD5_ParseExpect(&s, "("); + tc->st[0] = MD5_ParseFloat(&s); + tc->st[1] = MD5_ParseFloat(&s); + MD5_ParseExpect(&s, ")"); md5_vertex_t *vert = &mesh->vertices[vert_index]; - MD5_UINT(vert->start); - MD5_UINT(vert->count); + vert->start = MD5_ParseUint(&s, 0, UINT16_MAX); + vert->count = MD5_ParseUint(&s, 0, UINT16_MAX); } - MD5_EXPECT("numtris"); - MD5_UINT(num_tris); - MD5_ENSURE(num_tris <= TESS_MAX_INDICES / 3, "Too many tris"); - OOM_CHECK(mesh->indices = MD5_Malloc(num_tris * 3 * sizeof(mesh->indices[0]))); + MD5_ParseExpect(&s, "numtris"); + uint32_t num_tris = MD5_ParseUint(&s, 0, TESS_MAX_INDICES / 3); + mesh->indices = MD5_Malloc(model, num_tris * 3 * sizeof(mesh->indices[0])); mesh->num_indices = num_tris * 3; for (j = 0; j < num_tris; j++) { - MD5_EXPECT("tri"); - - uint32_t tri_index; - MD5_UINT(tri_index); - MD5_ENSURE(tri_index < num_tris, "Bad tri index"); - - for (k = 0; k < 3; k++) { - uint32_t vert_index; - MD5_UINT(vert_index); - MD5_ENSURE(vert_index < mesh->num_verts, "Bad tri vert"); - mesh->indices[tri_index * 3 + k] = vert_index; - } + MD5_ParseExpect(&s, "tri"); + uint32_t tri_index = MD5_ParseUint(&s, 0, num_tris - 1); + for (k = 0; k < 3; k++) + mesh->indices[tri_index * 3 + k] = MD5_ParseUint(&s, 0, mesh->num_verts - 1); } - MD5_EXPECT("numweights"); - MD5_UINT(num_weights); - MD5_ENSURE(num_weights <= MD5_MAX_WEIGHTS, "Too many weights"); - OOM_CHECK(mesh->weights = MD5_Malloc(num_weights * sizeof(mesh->weights[0]))); - mesh->num_weights = num_weights; + MD5_ParseExpect(&s, "numweights"); + mesh->num_weights = MD5_ParseUint(&s, 0, MD5_MAX_WEIGHTS); + mesh->weights = MD5_Malloc(model, mesh->num_weights * sizeof(mesh->weights [0])); + mesh->jointnums = MD5_Malloc(model, mesh->num_weights * sizeof(mesh->jointnums[0])); - for (j = 0; j < num_weights; j++) { - MD5_EXPECT("weight"); + for (j = 0; j < mesh->num_weights; j++) { + MD5_ParseExpect(&s, "weight"); - uint32_t weight_index; - MD5_UINT(weight_index); - MD5_ENSURE(weight_index < num_weights, "Bad weight index"); + uint32_t weight_index = MD5_ParseUint(&s, 0, mesh->num_weights - 1); + mesh->jointnums[weight_index] = MD5_ParseUint(&s, 0, mdl->num_joints - 1); md5_weight_t *weight = &mesh->weights[weight_index]; - - uint32_t joint; - MD5_UINT(joint); - MD5_ENSURE(joint < mdl->num_joints, "Bad weight joint"); - weight->joint = joint; - - MD5_FLOAT(weight->bias); - MD5_VECTOR(weight->pos); + weight->bias = MD5_ParseFloat(&s); + MD5_ParseVector(&s, weight->pos); } - MD5_EXPECT("}"); + MD5_ParseExpect(&s, "}"); // check integrity of data; this has to be done last // because of circular data dependencies - for (j = 0; j < num_verts; j++) { + for (j = 0; j < mesh->num_verts; j++) { md5_vertex_t *vert = &mesh->vertices[j]; - MD5_ENSURE((uint64_t)vert->start + vert->count <= num_weights, "Bad vert start/count"); + if (vert->start + vert->count > mesh->num_weights) + MD5_ParseError("Bad vert start/count"); } - MD5_ComputeNormals(mesh, mdl->base_skeleton); + MD5_ComputeNormals(mesh, base_skeleton); } - FS_FreeFile(buffer); return true; - -fail: - MOD_PrintError(path, ret); - FS_FreeFile(buffer); - return false; } typedef struct { char name[MD5_MAX_JOINTNAME]; - uint32_t parent; - uint32_t flags; - uint32_t start_index; + int parent, flags, start_index; bool scale_pos; } joint_info_t; -typedef struct { - vec3_t pos; - quat_t orient; -} baseframe_joint_t; - #define MD5_NUM_ANIMATED_COMPONENT_BITS 6 /** @@ -1057,21 +1031,21 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, Quat_ComputeW(animated_quat); - // parent should already be calculated md5_joint_t *thisJoint = &skeleton_frame[i]; if (joint_infos[i].scale_pos) VectorScale(animated_position, thisJoint->scale, animated_position); - int parent = thisJoint->parent = (int32_t)joint_infos[i].parent; + int parent = joint_infos[i].parent; if (parent < 0) { VectorCopy(animated_position, thisJoint->pos); Vector4Copy(animated_quat, thisJoint->orient); continue; } - Q_assert(parent < num_joints); - md5_joint_t *parentJoint = &skeleton_frame[parent]; + // parent should already be calculated + Q_assert(parent < i); + const md5_joint_t *parentJoint = &skeleton_frame[parent]; // add positions vec3_t rotated_pos; @@ -1087,7 +1061,7 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, /** * Parse some JSON vomit. Don't ask. */ -static void MOD_LoadMD5Scale(md5_model_t *model, const char *path, joint_info_t *joint_infos) +static void MD5_LoadScales(const md5_model_t *model, const char *path, joint_info_t *joint_infos) { const jsmntok_t *tok, *end; jsmn_parser parser; @@ -1175,66 +1149,60 @@ static void MOD_LoadMD5Scale(md5_model_t *model, const char *path, joint_info_t /** * Load an MD5 animation from file. */ -static bool MOD_LoadMD5Anim(model_t *model, const char *path) +static bool MD5_ParseAnim(model_t *model, const char *s, const char *path) { joint_info_t joint_infos[MD5_MAX_JOINTS]; baseframe_joint_t base_frame[MD5_MAX_JOINTS]; float anim_frame_data[MD5_MAX_JOINTS * MD5_NUM_ANIMATED_COMPONENT_BITS]; - uint32_t num_frames, num_joints, num_animated_components; + int num_joints, num_animated_components; md5_model_t *mdl = model->skeleton; - int i, j, ret; - void *buffer; - const char *s; + int i, j; - ret = FS_LoadFile(path, &buffer); - if (!buffer) - goto fail; - s = buffer; + if (setjmp(md5_jmpbuf)) + return false; + + com_linenum = 1; // parse header - MD5_EXPECT("MD5Version"); - MD5_EXPECT("10"); + MD5_ParseExpect(&s, "MD5Version"); + MD5_ParseExpect(&s, "10"); - MD5_EXPECT("commandline"); - MD5_SKIP(); + MD5_ParseExpect(&s, "commandline"); + COM_SkipToken(&s); - MD5_EXPECT("numFrames"); - MD5_UINT(num_frames); - // md5 replacements need at least 1 frame, because the + MD5_ParseExpect(&s, "numFrames"); + // MD5 replacements need at least 1 frame, because the // pose frame isn't used - MD5_ENSURE(num_frames > 0, "No frames"); - MD5_ENSURE(num_frames <= MD5_MAX_FRAMES, "Too many frames"); - mdl->num_frames = num_frames; + mdl->num_frames = MD5_ParseUint(&s, 1, MD5_MAX_FRAMES); // warn on mismatched frame counts (not fatal) if (mdl->num_frames != model->numframes) Com_WPrintf("%s doesn't match frame count for %s (%i vs %i)\n", path, model->name, mdl->num_frames, model->numframes); - MD5_EXPECT("numJoints"); - MD5_UINT(num_joints); - MD5_ENSURE(num_joints == mdl->num_joints, "Bad numJoints"); + MD5_ParseExpect(&s, "numJoints"); + num_joints = MD5_ParseUint(&s, 1, MD5_MAX_JOINTS); + if (num_joints != mdl->num_joints) + MD5_ParseError("Bad numJoints"); - MD5_EXPECT("frameRate"); - MD5_SKIP(); + MD5_ParseExpect(&s, "frameRate"); + COM_SkipToken(&s); - MD5_EXPECT("numAnimatedComponents"); - MD5_UINT(num_animated_components); - MD5_ENSURE(num_animated_components <= q_countof(anim_frame_data), "Bad numAnimatedComponents"); + MD5_ParseExpect(&s, "numAnimatedComponents"); + num_animated_components = MD5_ParseUint(&s, 0, q_countof(anim_frame_data)); - MD5_EXPECT("hierarchy"); - MD5_EXPECT("{"); + MD5_ParseExpect(&s, "hierarchy"); + MD5_ParseExpect(&s, "{"); for (i = 0; i < mdl->num_joints; i++) { joint_info_t *joint_info = &joint_infos[i]; - Q_strlcpy(joint_info->name, COM_Parse(&s), sizeof(joint_info->name)); + COM_ParseToken(&s, joint_info->name, sizeof(joint_info->name)); - MD5_UINT(joint_info->parent); - MD5_UINT(joint_info->flags); - MD5_UINT(joint_info->start_index); - - joint_info->scale_pos = false; + joint_info->parent = MD5_ParseInt (&s, -1, mdl->num_joints - 1); + joint_info->flags = MD5_ParseUint(&s, 0, UINT32_MAX); + joint_info->start_index = MD5_ParseUint(&s, 0, num_animated_components); + joint_info->scale_pos = false; // validate animated components int num_components = 0; @@ -1243,42 +1211,44 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) if (joint_info->flags & BIT(j)) num_components++; - MD5_ENSURE((uint64_t)joint_info->start_index + num_components <= num_animated_components, "Bad joint info"); + if (joint_info->start_index + num_components > num_animated_components) + MD5_ParseError("Bad joint info"); - // validate parents; they need to match the base skeleton - MD5_ENSURE(joint_info->parent == mdl->base_skeleton[i].parent, "Bad parent joint"); + // parent must be -1 or already processed joint + if (joint_info->parent >= i) + MD5_ParseError("Bad parent joint"); } - MD5_EXPECT("}"); + MD5_ParseExpect(&s, "}"); // bounds are ignored and are apparently usually wrong anyways - // so we'll just rely on them being "replacement" md2s/md3s. - // the md2/md3 ones are used instead. - MD5_EXPECT("bounds"); - MD5_EXPECT("{"); + // so we'll just rely on them being "replacement" MD2s/MD3s. + // the MD2/MD3 ones are used instead. + MD5_ParseExpect(&s, "bounds"); + MD5_ParseExpect(&s, "{"); for (i = 0; i < mdl->num_frames * 2; i++) { vec3_t dummy; - MD5_VECTOR(dummy); + MD5_ParseVector(&s, dummy); } - MD5_EXPECT("}"); + MD5_ParseExpect(&s, "}"); - MD5_EXPECT("baseframe"); - MD5_EXPECT("{"); + MD5_ParseExpect(&s, "baseframe"); + MD5_ParseExpect(&s, "{"); for (i = 0; i < mdl->num_joints; i++) { baseframe_joint_t *base_joint = &base_frame[i]; - MD5_VECTOR(base_joint->pos); - MD5_VECTOR(base_joint->orient); + MD5_ParseVector(&s, base_joint->pos); + MD5_ParseVector(&s, base_joint->orient); Quat_ComputeW(base_joint->orient); } - MD5_EXPECT("}"); + MD5_ParseExpect(&s, "}"); - OOM_CHECK(mdl->skeleton_frames = MD5_Malloc(sizeof(mdl->skeleton_frames[0]) * mdl->num_frames * mdl->num_joints)); + mdl->skeleton_frames = MD5_Malloc(model, sizeof(mdl->skeleton_frames[0]) * mdl->num_frames * mdl->num_joints); // initialize scales for (i = 0; i < mdl->num_frames * mdl->num_joints; i++) @@ -1288,34 +1258,45 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) char scale_path[MAX_QPATH]; if (COM_StripExtension(scale_path, path, sizeof(scale_path)) < sizeof(scale_path) && Q_strlcat(scale_path, ".md5scale", sizeof(scale_path)) < sizeof(scale_path)) - MOD_LoadMD5Scale(model->skeleton, scale_path, joint_infos); + MD5_LoadScales(model->skeleton, scale_path, joint_infos); else Com_WPrintf("MD5 scale path too long: %s\n", scale_path); for (i = 0; i < mdl->num_frames; i++) { - MD5_EXPECT("frame"); + MD5_ParseExpect(&s, "frame"); - uint32_t frame_index; - MD5_UINT(frame_index); - MD5_ENSURE(frame_index < mdl->num_frames, "Bad frame index"); + uint32_t frame_index = MD5_ParseUint(&s, 0, mdl->num_frames - 1); - MD5_EXPECT("{"); + MD5_ParseExpect(&s, "{"); for (j = 0; j < num_animated_components; j++) - MD5_FLOAT(anim_frame_data[j]); - MD5_EXPECT("}"); + anim_frame_data[j] = MD5_ParseFloat(&s); + MD5_ParseExpect(&s, "}"); /* Build frame skeleton from the collected data */ MD5_BuildFrameSkeleton(joint_infos, base_frame, anim_frame_data, &mdl->skeleton_frames[frame_index * mdl->num_joints], mdl->num_joints); } - FS_FreeFile(buffer); return true; +} -fail: - MOD_PrintError(path, ret); - FS_FreeFile(buffer); - return false; +static bool MD5_LoadFile(model_t *model, const char *path, bool (*parse)(model_t *, const char *, const char *)) +{ + void *data; + int ret = FS_LoadFile(path, &data); + if (!data) { + MOD_PrintError(path, ret); + return false; + } + + ret = parse(model, data, path); + FS_FreeFile(data); + if (!ret) { + MOD_PrintError(path, Q_ERR_INVALID_FORMAT); + return false; + } + + return true; } static bool MD5_LoadSkins(model_t *model) @@ -1365,10 +1346,9 @@ static void MOD_LoadMD5(model_t *model) if (!FS_FileExists(mesh_path) || !FS_FileExists(anim_path)) return; - if (!MOD_LoadMD5Mesh(model, mesh_path)) + if (!MD5_LoadFile(model, mesh_path, MD5_ParseMesh)) goto fail; - - if (!MOD_LoadMD5Anim(model, anim_path)) + if (!MD5_LoadFile(model, anim_path, MD5_ParseAnim)) goto fail; if (!MD5_LoadSkins(model)) goto fail; From bfa623d48bdde02d9b9fa9fa0242089a98f5c416 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:01:00 +0300 Subject: [PATCH 31/43] Simplify Quat_SLerp() close angle check. --- src/common/math.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/math.c b/src/common/math.c index 53659ca81..4aca75c55 100644 --- a/src/common/math.c +++ b/src/common/math.c @@ -418,7 +418,7 @@ void Quat_ComputeW(quat_t q) } } -#define QUAT_EPSILON 0.000001f +#define DOT_THRESHOLD 0.9995f void Quat_SLerp(const quat_t qa, const quat_t qb, float backlerp, float frontlerp, quat_t out) { @@ -453,7 +453,7 @@ void Quat_SLerp(const quat_t qa, const quat_t qb, float backlerp, float frontler // compute interpolation fraction float k0, k1; - if (1.0f - cosOmega <= QUAT_EPSILON) { + if (cosOmega > DOT_THRESHOLD) { // very close - just use linear interpolation k0 = backlerp; k1 = frontlerp; From 782c15bfcec4cdea2591da6ee19afc00532fdf72 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 21:21:22 +0300 Subject: [PATCH 32/43] Add VectorRotate() macro. --- inc/shared/shared.h | 10 ++++++---- src/client/newfx.c | 4 +--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index abf69c6d7..233384809 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -231,6 +231,11 @@ typedef struct { ((d)[0]=(a)[0]+(b)[0]*(c)[0], \ (d)[1]=(a)[1]+(b)[1]*(c)[1], \ (d)[2]=(a)[2]+(b)[2]*(c)[2]) +#define VectorRotate(in,axis,out) \ + ((out)[0]=DotProduct(in,(axis)[0]), \ + (out)[1]=DotProduct(in,(axis)[1]), \ + (out)[2]=DotProduct(in,(axis)[2])) + #define VectorEmpty(v) ((v)[0]==0&&(v)[1]==0&&(v)[2]==0) #define VectorCompare(v1,v2) ((v1)[0]==(v2)[0]&&(v1)[1]==(v2)[1]&&(v1)[2]==(v2)[2]) #define VectorLength(v) (sqrtf(DotProduct((v),(v)))) @@ -295,11 +300,8 @@ static inline void TransposeAxis(vec3_t axis[3]) static inline void RotatePoint(vec3_t point, const vec3_t axis[3]) { vec3_t temp; - VectorCopy(point, temp); - point[0] = DotProduct(temp, axis[0]); - point[1] = DotProduct(temp, axis[1]); - point[2] = DotProduct(temp, axis[2]); + VectorRotate(temp, axis, point); } static inline uint32_t Q_npot32(uint32_t k) diff --git a/src/client/newfx.c b/src/client/newfx.c index 7dd4fedd7..70650217d 100644 --- a/src/client/newfx.c +++ b/src/client/newfx.c @@ -990,9 +990,7 @@ void CL_HologramParticles(const vec3_t org) p->time = cl.time; p->color = 0xd0; - VectorCopy(bytedirs[i], dir); - RotatePoint(dir, axis); - + VectorRotate(bytedirs[i], axis, dir); VectorMA(org, 100.0f, dir, p->org); VectorClear(p->vel); From 81b2eb1d2e775e1ce4d48e44710fc62c75f273bc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:01:00 +0300 Subject: [PATCH 33/43] Optimize MD5 interpolation more. Get rid of Quat_RotatePoint() in hot path. Replace it with matrix multiply. Gives some nice FPS boost. --- inc/common/math.h | 20 ++++++++++++++++++++ inc/shared/platform.h | 4 ++++ src/refresh/gl.h | 1 + src/refresh/mesh.c | 32 +++++++++++++++++++++----------- src/refresh/models.c | 3 +++ 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/inc/common/math.h b/inc/common/math.h index 91ce8c4be..69d1ade0a 100644 --- a/inc/common/math.h +++ b/inc/common/math.h @@ -124,6 +124,26 @@ static inline void Quat_RotatePoint(const quat_t q, const vec3_t in, vec3_t out) out[Z] = output[Z]; } +static inline void Quat_ToAxis(const quat_t q, vec3_t axis[3]) +{ + float q0 = q[W]; + float q1 = q[X]; + float q2 = q[Y]; + float q3 = q[Z]; + + axis[0][0] = 2 * (q0 * q0 + q1 * q1) - 1; + axis[0][1] = 2 * (q1 * q2 - q0 * q3); + axis[0][2] = 2 * (q1 * q3 + q0 * q2); + + axis[1][0] = 2 * (q1 * q2 + q0 * q3); + axis[1][1] = 2 * (q0 * q0 + q2 * q2) - 1; + axis[1][2] = 2 * (q2 * q3 - q0 * q1); + + axis[2][0] = 2 * (q1 * q3 - q0 * q2); + axis[2][1] = 2 * (q2 * q3 + q0 * q1); + axis[2][2] = 2 * (q0 * q0 + q3 * q3) - 1; +} + #undef X #undef Y #undef Z diff --git a/inc/shared/platform.h b/inc/shared/platform.h index 184c87337..9f074fd70 100644 --- a/inc/shared/platform.h +++ b/inc/shared/platform.h @@ -139,6 +139,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #define q_unreachable() abort() #endif +#define q_forceinline inline __attribute__((always_inline)) + #else /* __GNUC__ */ #ifdef _MSC_VER @@ -147,12 +149,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #define q_malloc __declspec(restrict) #define q_alignof(t) __alignof(t) #define q_unreachable() __assume(0) +#define q_forceinline __forceinline #else #define q_noreturn #define q_noinline #define q_malloc #define q_alignof(t) 1 #define q_unreachable() abort() +#define q_forceinline inline #endif #define q_printf(f, a) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 920868ea1..b9fcd5b02 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -330,6 +330,7 @@ typedef struct { vec3_t pos; float scale; quat_t orient; + vec3_t axis[3]; } md5_joint_t; /* Vertex */ diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 487c6f7cd..a30231513 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -668,13 +668,17 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, #if USE_MD5 +#if (defined __OPTIMIZE__) && (defined __GNUC__) && !(defined __clang__) +#pragma GCC optimize("O3") +#endif + // for the given vertex, set of weights & skeleton, calculate // the output vertex (and optionally normal). -static inline void calc_skel_vert(const md5_vertex_t *vert, - const md5_mesh_t *mesh, - const md5_joint_t *skeleton, - float *restrict out_position, - float *restrict out_normal) +static q_forceinline void calc_skel_vert(const md5_vertex_t *vert, + const md5_mesh_t *mesh, + const md5_joint_t *skeleton, + float *restrict out_position, + float *restrict out_normal) { VectorClear(out_position); @@ -686,13 +690,13 @@ static inline void calc_skel_vert(const md5_vertex_t *vert, const md5_joint_t *joint = &skeleton[mesh->jointnums[vert->start + i]]; vec3_t wv; - Quat_RotatePoint(joint->orient, weight->pos, wv); + VectorRotate(weight->pos, joint->axis, wv); VectorMA(joint->pos, joint->scale, wv, wv); VectorMA(out_position, weight->bias, wv, out_position); if (out_normal) { - Quat_RotatePoint(joint->orient, vert->normal, wv); + VectorRotate(vert->normal, joint->axis, wv); VectorMA(out_normal, weight->bias, wv, out_normal); } } @@ -738,14 +742,20 @@ static void lerp_alias_skeleton(const md5_model_t *model) unsigned frame_b = newframenum % model->num_frames; const md5_joint_t *skel_a = &model->skeleton_frames[frame_a * model->num_joints]; const md5_joint_t *skel_b = &model->skeleton_frames[frame_b * model->num_joints]; + md5_joint_t *out = temp_skeleton; - for (int i = 0; i < model->num_joints; i++) { - temp_skeleton[i].scale = skel_b[i].scale; - LerpVector2(skel_a[i].pos, skel_b[i].pos, backlerp, frontlerp, temp_skeleton[i].pos); - Quat_SLerp(skel_a[i].orient, skel_b[i].orient, backlerp, frontlerp, temp_skeleton[i].orient); + for (int i = 0; i < model->num_joints; i++, skel_a++, skel_b++, out++) { + out->scale = skel_b->scale; + LerpVector2(skel_a->pos, skel_b->pos, backlerp, frontlerp, out->pos); + Quat_SLerp(skel_a->orient, skel_b->orient, backlerp, frontlerp, out->orient); + Quat_ToAxis(out->orient, out->axis); } } +#if (defined __OPTIMIZE__) && (defined __GNUC__) && !(defined __clang__) +#pragma GCC reset_options +#endif + static void draw_skeleton_mesh(const md5_model_t *model, const md5_mesh_t *mesh, const md5_joint_t *skel) { if (glr.ent->flags & RF_SHELL_MASK) diff --git a/src/refresh/models.c b/src/refresh/models.c index 580d0dcf6..c747342e5 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1040,6 +1040,7 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, if (parent < 0) { VectorCopy(animated_position, thisJoint->pos); Vector4Copy(animated_quat, thisJoint->orient); + Quat_ToAxis(thisJoint->orient, thisJoint->axis); continue; } @@ -1055,6 +1056,8 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, // concat rotations Quat_MultiplyQuat(parentJoint->orient, animated_quat, thisJoint->orient); Quat_Normalize(thisJoint->orient); + + Quat_ToAxis(thisJoint->orient, thisJoint->axis); } } From 943a00c997ea1b6b112dbfbcc40f3c1d552e2fec Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:01:00 +0300 Subject: [PATCH 34/43] Un-inline quaternion functions. --- inc/common/math.h | 77 +++-------------------------------------------- src/common/math.c | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 72 deletions(-) diff --git a/inc/common/math.h b/inc/common/math.h index 69d1ade0a..8546f852a 100644 --- a/inc/common/math.h +++ b/inc/common/math.h @@ -73,82 +73,15 @@ void RotatePointAroundVector(vec3_t out, const vec3_t dir, const vec3_t in, floa // quaternion routines, for MD5 skeletons #if USE_MD5 - -#define X 0 -#define Y 1 -#define Z 2 -#define W 3 - typedef vec4_t quat_t; - void Quat_ComputeW(quat_t q); void Quat_SLerp(const quat_t qa, const quat_t qb, float backlerp, float frontlerp, quat_t out); float Quat_Normalize(quat_t q); - -static inline void Quat_MultiplyQuat(const quat_t qa, const quat_t qb, quat_t out) -{ - out[W] = (qa[W] * qb[W]) - (qa[X] * qb[X]) - (qa[Y] * qb[Y]) - (qa[Z] * qb[Z]); - out[X] = (qa[X] * qb[W]) + (qa[W] * qb[X]) + (qa[Y] * qb[Z]) - (qa[Z] * qb[Y]); - out[Y] = (qa[Y] * qb[W]) + (qa[W] * qb[Y]) + (qa[Z] * qb[X]) - (qa[X] * qb[Z]); - out[Z] = (qa[Z] * qb[W]) + (qa[W] * qb[Z]) + (qa[X] * qb[Y]) - (qa[Y] * qb[X]); -} - -static inline void Quat_MultiplyVector(const quat_t q, const vec3_t v, quat_t out) -{ - out[W] = -(q[X] * v[X]) - (q[Y] * v[Y]) - (q[Z] * v[Z]); - out[X] = (q[W] * v[X]) + (q[Y] * v[Z]) - (q[Z] * v[Y]); - out[Y] = (q[W] * v[Y]) + (q[Z] * v[X]) - (q[X] * v[Z]); - out[Z] = (q[W] * v[Z]) + (q[X] * v[Y]) - (q[Y] * v[X]); -} - -// Conjugate quaternion. Also, inverse, for unit quaternions (which MD5 quats are) -static inline void Quat_Conjugate(const quat_t in, quat_t out) -{ - out[W] = in[W]; - out[X] = -in[X]; - out[Y] = -in[Y]; - out[Z] = -in[Z]; -} - -static inline void Quat_RotatePoint(const quat_t q, const vec3_t in, vec3_t out) -{ - quat_t tmp, inv, output; - - // Assume q is unit quaternion - Quat_Conjugate(q, inv); - Quat_MultiplyVector(q, in, tmp); - Quat_MultiplyQuat(tmp, inv, output); - - out[X] = output[X]; - out[Y] = output[Y]; - out[Z] = output[Z]; -} - -static inline void Quat_ToAxis(const quat_t q, vec3_t axis[3]) -{ - float q0 = q[W]; - float q1 = q[X]; - float q2 = q[Y]; - float q3 = q[Z]; - - axis[0][0] = 2 * (q0 * q0 + q1 * q1) - 1; - axis[0][1] = 2 * (q1 * q2 - q0 * q3); - axis[0][2] = 2 * (q1 * q3 + q0 * q2); - - axis[1][0] = 2 * (q1 * q2 + q0 * q3); - axis[1][1] = 2 * (q0 * q0 + q2 * q2) - 1; - axis[1][2] = 2 * (q2 * q3 - q0 * q1); - - axis[2][0] = 2 * (q1 * q3 - q0 * q2); - axis[2][1] = 2 * (q2 * q3 + q0 * q1); - axis[2][2] = 2 * (q0 * q0 + q3 * q3) - 1; -} - -#undef X -#undef Y -#undef Z -#undef W - +void Quat_MultiplyQuat(const float *restrict qa, const float *restrict qb, quat_t out); +void Quat_MultiplyVector(const float *restrict q, const float *restrict v, quat_t out); +void Quat_Conjugate(const quat_t in, quat_t out); +void Quat_RotatePoint(const quat_t q, const vec3_t in, vec3_t out); +void Quat_ToAxis(const quat_t q, vec3_t axis[3]); #endif #endif // USE_REF diff --git a/src/common/math.c b/src/common/math.c index 4aca75c55..94f071dd2 100644 --- a/src/common/math.c +++ b/src/common/math.c @@ -490,6 +490,65 @@ float Quat_Normalize(quat_t q) return length; } +void Quat_MultiplyQuat(const float *restrict qa, const float *restrict qb, quat_t out) +{ + out[W] = (qa[W] * qb[W]) - (qa[X] * qb[X]) - (qa[Y] * qb[Y]) - (qa[Z] * qb[Z]); + out[X] = (qa[X] * qb[W]) + (qa[W] * qb[X]) + (qa[Y] * qb[Z]) - (qa[Z] * qb[Y]); + out[Y] = (qa[Y] * qb[W]) + (qa[W] * qb[Y]) + (qa[Z] * qb[X]) - (qa[X] * qb[Z]); + out[Z] = (qa[Z] * qb[W]) + (qa[W] * qb[Z]) + (qa[X] * qb[Y]) - (qa[Y] * qb[X]); +} + +void Quat_MultiplyVector(const float *restrict q, const float *restrict v, quat_t out) +{ + out[W] = -(q[X] * v[X]) - (q[Y] * v[Y]) - (q[Z] * v[Z]); + out[X] = (q[W] * v[X]) + (q[Y] * v[Z]) - (q[Z] * v[Y]); + out[Y] = (q[W] * v[Y]) + (q[Z] * v[X]) - (q[X] * v[Z]); + out[Z] = (q[W] * v[Z]) + (q[X] * v[Y]) - (q[Y] * v[X]); +} + +// Conjugate quaternion. Also, inverse, for unit quaternions (which MD5 quats are) +void Quat_Conjugate(const quat_t in, quat_t out) +{ + out[W] = in[W]; + out[X] = -in[X]; + out[Y] = -in[Y]; + out[Z] = -in[Z]; +} + +void Quat_RotatePoint(const quat_t q, const vec3_t in, vec3_t out) +{ + quat_t tmp, inv, output; + + // Assume q is unit quaternion + Quat_Conjugate(q, inv); + Quat_MultiplyVector(q, in, tmp); + Quat_MultiplyQuat(tmp, inv, output); + + out[X] = output[X]; + out[Y] = output[Y]; + out[Z] = output[Z]; +} + +void Quat_ToAxis(const quat_t q, vec3_t axis[3]) +{ + float q0 = q[W]; + float q1 = q[X]; + float q2 = q[Y]; + float q3 = q[Z]; + + axis[0][0] = 2 * (q0 * q0 + q1 * q1) - 1; + axis[0][1] = 2 * (q1 * q2 - q0 * q3); + axis[0][2] = 2 * (q1 * q3 + q0 * q2); + + axis[1][0] = 2 * (q1 * q2 + q0 * q3); + axis[1][1] = 2 * (q0 * q0 + q2 * q2) - 1; + axis[1][2] = 2 * (q2 * q3 - q0 * q1); + + axis[2][0] = 2 * (q1 * q3 - q0 * q2); + axis[2][1] = 2 * (q2 * q3 + q0 * q1); + axis[2][2] = 2 * (q0 * q0 + q3 * q3) - 1; +} + #endif // USE_MD5 #endif // USE_REF From c2cb036f075e6d7826813201cd9597c21dfdfa48 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 18:00:13 +0300 Subject: [PATCH 35/43] Only warn if MD5 has less frames than MD2. --- src/refresh/models.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/refresh/models.c b/src/refresh/models.c index c747342e5..b93b22693 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1179,9 +1179,9 @@ static bool MD5_ParseAnim(model_t *model, const char *s, const char *path) mdl->num_frames = MD5_ParseUint(&s, 1, MD5_MAX_FRAMES); // warn on mismatched frame counts (not fatal) - if (mdl->num_frames != model->numframes) - Com_WPrintf("%s doesn't match frame count for %s (%i vs %i)\n", - path, model->name, mdl->num_frames, model->numframes); + if (mdl->num_frames < model->numframes) + Com_WPrintf("%s has less frames than %s (%i < %i)\n", path, + model->name, mdl->num_frames, model->numframes); MD5_ParseExpect(&s, "numJoints"); num_joints = MD5_ParseUint(&s, 1, MD5_MAX_JOINTS); From 33599442cc087efc666858b7b28c5bfb70c180e7 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 4 Oct 2024 16:44:01 -0400 Subject: [PATCH 36/43] Added sv_load_ent toggle cvar --- doc/server.md | 3 +++ src/server/init.c | 4 +++- src/server/main.c | 1 + src/server/server.h | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/server.md b/doc/server.md index 84c5ccfac..1e25e3a45 100644 --- a/doc/server.md +++ b/doc/server.md @@ -828,6 +828,9 @@ also create an alias for the map. How to create such files is out of scope of this manual (search the internet for ‘r1q2 map override file generator’). +This is togglable with `load_ent` -- default enabled (`1`), a setting of +`0` will disable loading overrides of either type + ### map\_visibility\_patch Attempt to patch miscalculated visibility data for some well-known maps (q2dm1, q2dm3 and q2dm8 are patched so far), fixing disappearing walls diff --git a/src/server/init.c b/src/server/init.c index 359e33352..93a3ffc4a 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -241,6 +241,7 @@ static server_state_t get_server_state(const char *s) return ss_game; } +cvar_t *sv_load_ent; static bool parse_and_check_server(mapcmd_t *cmd, const char *server, bool nextserver) { char expanded[MAX_QPATH], *ch; @@ -286,7 +287,8 @@ static bool parse_and_check_server(mapcmd_t *cmd, const char *server, bool nexts break; default: - CM_LoadOverrides(&cmd->cm, cmd->server, sizeof(cmd->server)); // may override server! + if (sv_load_ent->integer) + CM_LoadOverrides(&cmd->cm, cmd->server, sizeof(cmd->server)); // may override server! if (Q_concat(expanded, sizeof(expanded), "maps/", cmd->server, ".bsp") < sizeof(expanded)) ret = CM_LoadMap(&cmd->cm, expanded); if (ret < 0) diff --git a/src/server/main.c b/src/server/main.c index c5006269e..b52b9f71d 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -2455,6 +2455,7 @@ void SV_Init(void) g_view_predict = Cvar_Get("g_view_predict", "0", CVAR_ROM); g_view_low = Cvar_Get("g_view_low", "0", CVAR_ROM); g_view_high = Cvar_Get("g_view_high", "0", CVAR_ROM); + sv_load_ent = Cvar_Get("sv_load_ent", "1", CVAR_LATCH); init_rate_limits(); diff --git a/src/server/server.h b/src/server/server.h index e9a0d4803..76cce74ae 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -582,6 +582,7 @@ extern cvar_t *sv_ghostime; extern client_t *sv_client; extern edict_t *sv_player; +extern cvar_t *sv_load_ent; //=========================================================== // From 15bf62b13e5ec6ef45ebc3ac62a7fe87885a3ee6 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 4 Oct 2024 16:44:48 -0400 Subject: [PATCH 37/43] Fixed documentation --- doc/server.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/server.md b/doc/server.md index 1e25e3a45..805528e3a 100644 --- a/doc/server.md +++ b/doc/server.md @@ -828,7 +828,7 @@ also create an alias for the map. How to create such files is out of scope of this manual (search the internet for ‘r1q2 map override file generator’). -This is togglable with `load_ent` -- default enabled (`1`), a setting of +This is togglable with `sv_load_ent` -- default enabled (`1`), a setting of `0` will disable loading overrides of either type ### map\_visibility\_patch From 59e3668f61ed991393c6c4a9f3c5eadfa97c92e8 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 7 Oct 2024 15:09:24 -0400 Subject: [PATCH 38/43] Needed to rename VectorRotate because it was reintroduced..? --- inc/shared/shared.h | 2 +- src/action/a_doorkick.c | 4 ++-- src/action/acesrc/acebot.h | 5 +++-- src/action/acesrc/acebot_cmds.c | 5 +++++ src/action/botlib/botlib_spawn.c | 4 ++++ src/action/botlib/botlib_utils.c | 2 +- src/action/g_main.c | 1 + src/action/g_save.c | 1 + src/action/g_spawn.c | 18 +++++++++++------- 9 files changed, 29 insertions(+), 13 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index ec39e80bc..e2e453c11 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -528,7 +528,7 @@ void PerpendicularVector (vec3_t dst, const vec3_t src); void RotatePointAroundVector (vec3_t dst, const vec3_t dir, const vec3_t point, float degrees); -void VectorRotate( vec3_t in, vec3_t angles, vec3_t out ); // a_doorkick.c +//void VectorRotate( vec3_t in, vec3_t angles, vec3_t out ); // a_doorkick.c void VectorRotate2( vec3_t v, float degrees ); void Q_srand(uint32_t seed); diff --git a/src/action/a_doorkick.c b/src/action/a_doorkick.c index b848ddb2b..2648786cd 100644 --- a/src/action/a_doorkick.c +++ b/src/action/a_doorkick.c @@ -30,7 +30,7 @@ extern void door_use(edict_t * self, edict_t * other, edict_t * activator); // needed for KickDoor -void VectorRotate(vec3_t in, vec3_t angles, vec3_t out) +void VectorRotate3(vec3_t in, vec3_t angles, vec3_t out) { float cv, sv, angle, tv; @@ -102,7 +102,7 @@ int KickDoor(trace_t * tr_old, edict_t * ent, vec3_t forward) VectorNormalize(forward); VectorNormalize(d_forward); VectorSet(right, 0, 90, 0); - VectorRotate(d_forward, right, d_forward); + VectorRotate3(d_forward, right, d_forward); d = DotProduct(forward, d_forward); if (tr.ent->spawnflags & DOOR_REVERSE) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 6a2abe14d..d2ac9ba99 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -118,10 +118,11 @@ typedef enum //rekkie -- BSP -- e #define MAX_BOTSKILL 10 +extern cvar_t *bot_enable; // Enable/disable bots toggle extern cvar_t *bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! -extern cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit +extern cvar_t *bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit extern cvar_t *bot_remember; // How long (in seconds) the bot remembers an enemy after visibility has been lost -extern cvar_t* bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight +extern cvar_t *bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight extern cvar_t *bot_showpath; // Show bot paths //AQ2 ADD diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 54cb68997..5d21aeaab 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -44,6 +44,11 @@ qboolean ACECM_Commands(edict_t *ent) cmd = gi.argv(0); + if (!bot_enable->value) { + gi.dprintf("bot_enable is 0; Bots are disabled\n"); + return true; + } + //if(Q_stricmp (cmd, "addnode") == 0 && debug_mode) // ent->last_node = ACEND_AddNode(ent,atoi(gi.argv(1))); diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index a84f4d518..4ef693826 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -2140,6 +2140,10 @@ void BOTLIB_DMBotCountManager(void) void BOTLIB_CheckBotRules(void) { + // Disable bot logic entirely + if (!bot_enable->value) + return; + if (matchmode->value) // Bots never allowed in matchmode return; diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index 707a714c2..15691d5a7 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -117,5 +117,5 @@ void BOTLIB_Debug(const char *debugmsg, ...) { if (!bot_debug->value) return; - gi.dprintf(debugmsg); + gi.dprintf("%s", debugmsg); } diff --git a/src/action/g_main.c b/src/action/g_main.c index 9325ee570..e1a890a57 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -483,6 +483,7 @@ cvar_t *ltk_routing; cvar_t *ltk_botfile; cvar_t *ltk_loadbots; //rekkie -- DEV_1 -- s +cvar_t* bot_enable; // Enable/Disable bots cvar_t* bot_showpath; cvar_t* bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit diff --git a/src/action/g_save.c b/src/action/g_save.c index ab58b11ee..9753cf3f8 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -681,6 +681,7 @@ void InitGame( void ) ltk_botfile = gi.cvar( "ltk_botfile", "botdata", 0); ltk_loadbots = gi.cvar( "ltk_loadbots", "1", 0); //rekkie -- DEV_1 -- s + bot_enable = gi.cvar("bot_enable", "0", CVAR_LATCH); bot_skill = gi.cvar("bot_skill", "7", 0); // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! bot_skill_threshold = gi.cvar("bot_skill_threshold", "0", 0); // Dynamic skill adjustment kicks in if a threshold has been hit bot_remember = gi.cvar("bot_remember", "15", 0); // How long (in seconds) the bot remembers an enemy after visibility has been lost diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 4b4f58b35..4b1ad89b2 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1576,12 +1576,14 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn //rekkie -- s #ifndef NO_BOTS - BOTLIB_InitNavigation(NULL); -#ifdef USE_ZLIB - BOTLIB_LoadNavCompressed(); -#else - BOTLIB_LoadNav(); -#endif + if (bot_enable->value) { + BOTLIB_InitNavigation(NULL); + #ifdef USE_ZLIB + BOTLIB_LoadNavCompressed(); + #else + BOTLIB_LoadNav(); + + #endif //ACEND_LoadAAS(false); // This will also generate AAS if it doesn't exist //ACEND_BSP(NULL); @@ -1593,11 +1595,13 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn memset(&botlib_noises, 0, sizeof(botlib_noises)); //rekkie -- Fake Bot Client -- s - gi.SV_BotClearClients(); // So the server can clear all fake bot clients + gi.SV_BotClearClients(); + //gi.SV_BotClearClients(); // So the server can clear all fake bot clients //rekkie -- Fake Bot Client -- e if(bot_personality->value) BOTLIB_PersonalityFile(); + } #endif //rekkie -- e } From feb25fdb4c89fec43ddcef23a460103764d5efb8 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 7 Oct 2024 15:23:15 -0400 Subject: [PATCH 39/43] Returning if bots are disabled --- src/action/acesrc/acebot_cmds.c | 1 - src/action/botlib/botlib_cmd.c | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 5d21aeaab..65aaefe37 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -45,7 +45,6 @@ qboolean ACECM_Commands(edict_t *ent) cmd = gi.argv(0); if (!bot_enable->value) { - gi.dprintf("bot_enable is 0; Bots are disabled\n"); return true; } diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index 3aac838d5..a35b0ac7b 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -10,6 +10,12 @@ qboolean BOTLIB_SV_Cmds(void) if (Q_stricmp(cmd, "bots") == 0) { + if (!bot_enable->value) { + gi.dprintf("%s: bot_enable is 0; Bots are disabled, desired bots: %d\n", __func__, bot_connections.desire_bots); + bot_connections.desire_bots = 0; + return true; + } + int cc = gi.argc(); gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if bots manually added @@ -264,6 +270,7 @@ qboolean BOTLIB_Commands(edict_t* ent) // ACEND_BSP(ent); // return true; // } + if (Q_stricmp(cmd, "randomize_team_names") == 0) // Manually randomize team names { BOTLIB_RandomizeTeamNames(ent); @@ -492,11 +499,13 @@ qboolean BOTLIB_Commands(edict_t* ent) else if (Q_stricmp(cmd, "nav_load") == 0) // Load bot nav from file { #ifdef USE_ZLIB - BOTLIB_LoadNavCompressed(); + if (bot_enable->value) { + BOTLIB_LoadNavCompressed(); #else - BOTLIB_LoadNav(); + BOTLIB_LoadNav(); #endif - return true; + return true; + } } else if (Q_stricmp(cmd, "nav_save") == 0) // Save bot nav to file { From 6032d73466c9a4dd791d80d36c5bdd973db64da2 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 7 Oct 2024 15:50:15 -0400 Subject: [PATCH 40/43] Removed extra debug info --- src/action/acesrc/acebot_cmds.c | 4 ---- src/action/botlib/botlib_cmd.c | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 65aaefe37..54cb68997 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -44,10 +44,6 @@ qboolean ACECM_Commands(edict_t *ent) cmd = gi.argv(0); - if (!bot_enable->value) { - return true; - } - //if(Q_stricmp (cmd, "addnode") == 0 && debug_mode) // ent->last_node = ACEND_AddNode(ent,atoi(gi.argv(1))); diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index a35b0ac7b..8e5068f56 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -11,9 +11,8 @@ qboolean BOTLIB_SV_Cmds(void) if (Q_stricmp(cmd, "bots") == 0) { if (!bot_enable->value) { - gi.dprintf("%s: bot_enable is 0; Bots are disabled, desired bots: %d\n", __func__, bot_connections.desire_bots); + gi.dprintf("%s: bot_enable is 0; Bots are disabled\n", __func__); bot_connections.desire_bots = 0; - return true; } int cc = gi.argc(); From a5e0aaab68f847f6a4afb8ba6aaac79f3c264c28 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 11 Oct 2024 13:49:31 -0400 Subject: [PATCH 41/43] Fixing bot scaling issues in teamplay --- src/action/botlib/botlib_spawn.c | 82 ++++++++++++++++---------------- src/action/g_save.c | 2 +- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 4ef693826..5a9ec5487 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1935,14 +1935,15 @@ int BOTLIB_TPBotTeamScaling(void) if (bot_connections.total_bots == total_bots_needed) { return 0; // Each team is populated, return 0 } else if (bot_connections.total_bots > total_bots_needed) { - return -1; // Bots were removed - } + bot_connections.desire_bots = total_bots_needed; + return -1; // Bots were removed + } // Calculate the percentage of bots relative to the max team size float percent = (float)bot_connections.total_bots / total_bots_needed; // Initialize scaling direction if not already set - if (bot_connections.scale_dn == false && bot_connections.scale_up == false) { + if (!bot_connections.scale_dn && !bot_connections.scale_up) { bot_connections.scale_up = true; } @@ -1959,56 +1960,56 @@ int BOTLIB_TPBotTeamScaling(void) bot_connections.scale_dn = false; } - // Decrease bots + // Adjust desire_bots based on scaling direction if (bot_connections.scale_dn) { bot_connections.desire_bots--; - } - - // Increase bots - if (bot_connections.scale_up) { + } else if (bot_connections.scale_up) { bot_connections.desire_bots++; } - // Don't let bots be greater than total_bots_needed + // Ensure desire_bots is within valid range if (bot_connections.desire_bots > total_bots_needed) { - bot_connections.desire_bots--; + bot_connections.desire_bots = total_bots_needed; } - - // Never go below 1 bot when bot_maxteam is active if (bot_connections.desire_bots < 1) { bot_connections.desire_bots = 1; } - // Remove excess bots if bot_maxteam is reduced - if (bot_connections.total_bots > total_bots_needed) { - bot_connections.total_bots = total_bots_needed; - return -1; // Bots were removed + return 1; // Continue scaling + } else { // bot_maxteam 0, just add bots + int total_players = bot_connections.total_bots + bot_connections.total_humans; + int desired_total_players = (int)bot_playercount->value; + + if (desired_total_players && total_players < desired_total_players) { + // Scale up bots when total players are less than desired + bot_connections.desire_bots = desired_total_players - bot_connections.total_humans; + bot_connections.scale_up = true; + bot_connections.scale_dn = false; + } else if (total_players > desired_total_players) { + // Scale down bots when total players exceed desired + bot_connections.desire_bots = desired_total_players - bot_connections.total_humans; + bot_connections.scale_up = false; + bot_connections.scale_dn = true; + } else { + // No scaling needed + bot_connections.scale_up = false; + bot_connections.scale_dn = false; } - return 1; // Continue scaling - } else { // bot_maxteam 0, just add bots - if (bot_connections.desire_bots != bot_connections.total_bots){ - if (bot_connections.desire_bots > bot_connections.total_bots) { - bot_connections.scale_up = true; - bot_connections.scale_dn = false; - } else { - bot_connections.scale_up = false; - bot_connections.scale_dn = true; - } - } else { - bot_connections.scale_up = false; - bot_connections.scale_dn = false; - } - } - if (bot_connections.scale_up){ - return 1; - } else if (bot_connections.scale_dn){ - return -1; - } else { - return 0; - } + // Ensure desire_bots is not negative + if (bot_connections.desire_bots < 0) { + bot_connections.desire_bots = 0; + } + } + + if (bot_connections.scale_up) { + return 1; + } else if (bot_connections.scale_dn) { + return -1; + } else { + return 0; + } } - void BOTLIB_TPBotCountManual(void) { // Sanity check @@ -2177,7 +2178,8 @@ void BOTLIB_CheckBotRules(void) } // Turn on team balance if bot_maxteam is used - if (bot_maxteam->value > 0) bot_connections.auto_balance_bots = true; + if (bot_maxteam->value > 0 || use_balancer->value) + bot_connections.auto_balance_bots = true; if (teamplay->value && !bot_connections.auto_balance_bots) { BOTLIB_TPBotCountManual(); diff --git a/src/action/g_save.c b/src/action/g_save.c index 9753cf3f8..a10dcad4c 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -687,7 +687,7 @@ void InitGame( void ) bot_remember = gi.cvar("bot_remember", "15", 0); // How long (in seconds) the bot remembers an enemy after visibility has been lost bot_reaction = gi.cvar("bot_reaction", "0.5", 0); // How long (in seconds) until the bot reacts to an enemy in sight bot_showpath = gi.cvar("bot_showpath", "0", 0); - bot_maxteam = gi.cvar("bot_maxteam", "10", 0); + bot_maxteam = gi.cvar("bot_maxteam", "0", 0); bot_playercount = gi.cvar("bot_playercount", "0", 0); bot_rush = gi.cvar("bot_rush", "0", 0); bot_randvoice = gi.cvar("bot_randvoice", "5", 0); From 8cb2d94bfb29e32f03df215c1010b1dfb4134b7f Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 11 Oct 2024 20:23:59 -0400 Subject: [PATCH 42/43] Commenting out TE_DAMAGE_DEALT --- src/action/p_view.c | 16 ++++++++-------- src/client/tent.c | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/action/p_view.c b/src/action/p_view.c index 8807b4c0e..1a7d1b414 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -1695,12 +1695,12 @@ void ClientEndServerFrame (edict_t * ent) RadioThink(ent); // Paril's hit markers (don't draw for bots!) - if (ent->client->damage_dealt > 0 && !ent->is_bot) - { - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_DAMAGE_DEALT); - gi.WriteShort(min(ent->client->damage_dealt, INT16_MAX)); - gi.unicast(ent, false); - ent->client->damage_dealt = 0; - } + // if (ent->client->damage_dealt > 0 && !ent->is_bot) + // { + // gi.WriteByte(svc_temp_entity); + // gi.WriteByte(TE_DAMAGE_DEALT); + // gi.WriteShort(min(ent->client->damage_dealt, INT16_MAX)); + // gi.unicast(ent, false); + // ent->client->damage_dealt = 0; + // } } diff --git a/src/client/tent.c b/src/client/tent.c index a0dfc13c8..b0060cdd3 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -1660,15 +1660,15 @@ void CL_ParseTEnt(void) CL_PowerSplash(); break; - case TE_DAMAGE_DEALT: - if (te.count > 0 && cl_hit_markers->integer > 0) { - cl.hit_marker_time = cls.realtime; - cl.hit_marker_count = te.count; - if (cl_hit_markers->integer > 1) { - S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); - } - } - break; + // case TE_DAMAGE_DEALT: + // if (te.count > 0 && cl_hit_markers->integer > 0) { + // cl.hit_marker_time = cls.realtime; + // cl.hit_marker_count = te.count; + // if (cl_hit_markers->integer > 1) { + // S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); + // } + // } + // break; default: Com_Error(ERR_DROP, "%s: bad type", __func__); From d54bdc6995025c272425d8c90f6912ddc303f037 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 14 Oct 2024 10:56:57 -0400 Subject: [PATCH 43/43] No longer precaching AQtion gun sounds, saves us memory and sound slots --- src/client/tent.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/tent.c b/src/client/tent.c index b0060cdd3..ebdc91d92 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -315,7 +315,9 @@ void CL_RegisterTEntSounds(void) } CL_RegisterFootsteps(); - CL_RegisterAQtionSounds(); + + // Commenting out until better support is added, else we're just wasting memory and sound slots + //CL_RegisterAQtionSounds(); cl_sfx_lightning = S_RegisterSound("weapons/tesla.wav"); cl_sfx_disrexp = S_RegisterSound("weapons/disrupthit.wav");