From 60dfabbeb7a50cd55b214194eb2ec64c39529b59 Mon Sep 17 00:00:00 2001 From: Jack Slater Date: Thu, 18 Apr 2024 08:45:56 -1000 Subject: [PATCH 01/34] Update SECURITY.md --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 8732e8b1..fd5c9f37 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ We take security very seriously at ioquake3. We welcome any peer review of our 1 ### Where should I report security issues? In order to give the community time to respond and upgrade we strongly urge you report all security issues privately. -Please e-mail zachary@ioquake.org directly to provide details and repro steps and we will respond as soon as possible, but please note: +Please e-mail jack@ioquake.org directly to provide details and repro steps and we will respond as soon as possible, but please note: ### This is an entirely free software project without much in the way of external funding or sponsorships. ### We cannot guarantee quick responses but we very much appreciate your discretion when reporting security vulnerabilities. From aecf3f55cefed379927638d6fb3f867fcb1f3571 Mon Sep 17 00:00:00 2001 From: Alan Zhao Date: Wed, 15 May 2024 15:14:52 -0700 Subject: [PATCH 02/34] Use nanosleep(2) instead of usleep(3) usleep(3) was declared obsolete in POSIX.1-2001 and removed in POSIX.1-2008 and nanosleep(2) was recommended to be used instead. --- code/autoupdater/autoupdater.c | 5 ++++- code/sys/sys_unix.c | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/code/autoupdater/autoupdater.c b/code/autoupdater/autoupdater.c index 4a052027..e013a1f4 100644 --- a/code/autoupdater/autoupdater.c +++ b/code/autoupdater/autoupdater.c @@ -933,10 +933,13 @@ static void waitToApplyUpdates(void) OS forcibly closes the pipe), we will unblock. Then we can loop on kill() until the process is truly gone. */ int x = 0; + struct timespec req; + req.tv_sec = 0; + req.tv_nsec = 100000000; read(3, &x, sizeof (x)); info("Pipe has closed, waiting for process to fully go away now."); while (kill(options.waitforprocess, 0) == 0) { - usleep(100000); + nanosleep(&req, NULL); } #endif } diff --git a/code/sys/sys_unix.c b/code/sys/sys_unix.c index 66b6fa17..db29e8f0 100644 --- a/code/sys/sys_unix.c +++ b/code/sys/sys_unix.c @@ -38,6 +38,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include #include #include +#include qboolean stdinIsATTY; @@ -548,11 +549,15 @@ void Sys_Sleep( int msec ) } else { + struct timespec req; + // With nothing to select() on, we can't wait indefinitely if( msec < 0 ) msec = 10; - usleep( msec * 1000 ); + req.tv_sec = msec/1000; + req.tv_nsec = (msec%1000)*1000000; + nanosleep(&req, NULL); } } From 735d987303f79b8873e0b05317f156acf7f03a76 Mon Sep 17 00:00:00 2001 From: rilysh Date: Sat, 18 May 2024 12:10:32 +0530 Subject: [PATCH 03/34] Makefile: fix compilation on FreeBSD --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 25ba0a9f..f90c785c 100644 --- a/Makefile +++ b/Makefile @@ -805,6 +805,8 @@ else # ifdef MINGW ############################################################################# ifeq ($(PLATFORM),freebsd) + # Use the default C compiler + TOOLS_CC=cc # flags BASE_CFLAGS = \ From 1fc83e4845b53a9b259f5980468c8db15fce8de7 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Mon, 20 May 2024 14:04:23 -0500 Subject: [PATCH 04/34] Update macOS version for GitHub CI macOS 11 runner image is scheduled to be removed next month. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d2cc660..1e13814a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: path: build/*.zip macos: name: macOS - runs-on: macos-11 + runs-on: macos-12 steps: - uses: actions/checkout@v4 - name: Compile From 3b984d2b510b491a315f455184265b6c6c896b17 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Wed, 5 Jun 2024 21:33:08 -0500 Subject: [PATCH 05/34] OpenGL2: Add OpenGL ES 2.0+ support This mainly targets OpenGL ES 2.0 but it also supports compiling GLSL as ESSL 3.00. It's missing support for framebuffer objects which should be possible on ES 2. (Though using renderbuffers instead of textures.) opengl1 cvars that are not supported will display a message and disable the cvar. This has not been reviewed for new opengl2 cvars. Enabling cvars may cause rendering issues. Some of the broken cvars may be possible to support using OpenGL ES 3 features. The game displays okay with the default cvars. --- README.md | 15 +++ code/client/cl_avi.c | 5 +- code/renderergl2/tr_backend.c | 29 ++++- code/renderergl2/tr_bsp.c | 2 +- code/renderergl2/tr_cmds.c | 15 ++- code/renderergl2/tr_extensions.c | 108 +++++++++++++++-- code/renderergl2/tr_flares.c | 8 ++ code/renderergl2/tr_glsl.c | 177 ++++++++++++++++++---------- code/renderergl2/tr_image.c | 188 +++++++++++++++++++++++++++--- code/renderergl2/tr_init.c | 83 ++++++++++--- code/renderergl2/tr_local.h | 9 ++ code/renderergl2/tr_postprocess.c | 2 + code/sdl/sdl_glimp.c | 134 +++++++++++++++------ opengl2-readme.md | 8 ++ 14 files changed, 637 insertions(+), 146 deletions(-) diff --git a/README.md b/README.md index ce2999cb..f2031c73 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,21 @@ Makefile.local: The defaults for these variables differ depending on the target platform. +# OpenGL ES support + +The opengl2 renderer (the default) supports OpenGL ES 2+. Though there +are many missing features and the performance may not be sufficient for +embedded System-on-a-Chip and mobile platforms. + +The opengl1 renderer does not have OpenGL ES support. + +The `r_useOpenGLES` cvar controls whether to use OpenGL or OpenGL ES API. +Set to -1 for auto (default), 0 for OpenGL, and 1 for OpenGL ES. It should be +set using command line arguments: + + ioquake3 +set cl_renderer opengl2 +set r_useOpenGLES 1 + + # Console ## New cvars diff --git a/code/client/cl_avi.c b/code/client/cl_avi.c index 2f0dffd6..00cf7b1d 100644 --- a/code/client/cl_avi.c +++ b/code/client/cl_avi.c @@ -357,11 +357,12 @@ qboolean CL_OpenAVIForWriting( const char *fileName ) else afd.motionJpeg = qfalse; - // Buffers only need to store RGB pixels. + // Capture buffer stores RGB pixels but OpenGL ES reads RGBA and converts to RGB in-place. + // Encode buffer only needs to store RGB pixels. // Allocate a bit more space for the capture buffer to account for possible // padding at the end of pixel lines, and padding for alignment #define MAX_PACK_LEN 16 - afd.cBuffer = Z_Malloc((afd.width * 3 + MAX_PACK_LEN - 1) * afd.height + MAX_PACK_LEN - 1); + afd.cBuffer = Z_Malloc((afd.width * 4 + MAX_PACK_LEN - 1) * afd.height + MAX_PACK_LEN - 1); // raw avi files have pixel lines start on 4-byte boundaries afd.eBuffer = Z_Malloc(PAD(afd.width * 3, AVI_LINE_PADDING) * afd.height); diff --git a/code/renderergl2/tr_backend.c b/code/renderergl2/tr_backend.c index 0cffa4ee..71548779 100644 --- a/code/renderergl2/tr_backend.c +++ b/code/renderergl2/tr_backend.c @@ -732,6 +732,7 @@ void RE_StretchRaw (int x, int y, int w, int h, int cols, int rows, const byte * } void RE_UploadCinematic (int w, int h, int cols, int rows, const byte *data, int client, qboolean dirty) { + byte *buffer; GLuint texture; if (!tr.scratchImage[client]) @@ -746,7 +747,18 @@ void RE_UploadCinematic (int w, int h, int cols, int rows, const byte *data, int if ( cols != tr.scratchImage[client]->width || rows != tr.scratchImage[client]->height ) { tr.scratchImage[client]->width = tr.scratchImage[client]->uploadWidth = cols; tr.scratchImage[client]->height = tr.scratchImage[client]->uploadHeight = rows; - qglTextureImage2DEXT(texture, GL_TEXTURE_2D, 0, GL_RGB8, cols, rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + + if ( qglesMajorVersion >= 1 ) { + buffer = ri.Hunk_AllocateTempMemory( 3 * cols * rows ); + + R_ConvertTextureFormat( data, cols, rows, GL_RGB, GL_UNSIGNED_BYTE, buffer ); + qglTextureImage2DEXT(texture, GL_TEXTURE_2D, 0, GL_RGB, cols, rows, 0, GL_RGB, GL_UNSIGNED_BYTE, buffer); + + ri.Hunk_FreeTempMemory( buffer ); + } else { + qglTextureImage2DEXT(texture, GL_TEXTURE_2D, 0, GL_RGB8, cols, rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + } + qglTextureParameterfEXT(texture, GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); qglTextureParameterfEXT(texture, GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); qglTextureParameterfEXT(texture, GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); @@ -755,7 +767,16 @@ void RE_UploadCinematic (int w, int h, int cols, int rows, const byte *data, int if (dirty) { // otherwise, just subimage upload it so that drivers can tell we are going to be changing // it and don't try and do a texture compression - qglTextureSubImage2DEXT(texture, GL_TEXTURE_2D, 0, 0, 0, cols, rows, GL_RGBA, GL_UNSIGNED_BYTE, data); + if ( qglesMajorVersion >= 1 ) { + buffer = ri.Hunk_AllocateTempMemory( 3 * cols * rows ); + + R_ConvertTextureFormat( data, cols, rows, GL_RGB, GL_UNSIGNED_BYTE, buffer ); + qglTextureSubImage2DEXT(texture, GL_TEXTURE_2D, 0, 0, 0, cols, rows, GL_RGB, GL_UNSIGNED_BYTE, buffer); + + ri.Hunk_FreeTempMemory( buffer ); + } else { + qglTextureSubImage2DEXT(texture, GL_TEXTURE_2D, 0, 0, 0, cols, rows, GL_RGBA, GL_UNSIGNED_BYTE, data); + } } } } @@ -1140,14 +1161,14 @@ const void *RB_DrawSurfs( const void *data ) { if (glRefConfig.occlusionQuery) { tr.sunFlareQueryActive[tr.sunFlareQueryIndex] = qtrue; - qglBeginQuery(GL_SAMPLES_PASSED, tr.sunFlareQuery[tr.sunFlareQueryIndex]); + qglBeginQuery(glRefConfig.occlusionQueryTarget, tr.sunFlareQuery[tr.sunFlareQueryIndex]); } RB_DrawSun(0.3, tr.sunFlareShader); if (glRefConfig.occlusionQuery) { - qglEndQuery(GL_SAMPLES_PASSED); + qglEndQuery(glRefConfig.occlusionQueryTarget); } FBO_Bind(oldFbo); diff --git a/code/renderergl2/tr_bsp.c b/code/renderergl2/tr_bsp.c index 15f5f3cd..57a4efc3 100644 --- a/code/renderergl2/tr_bsp.c +++ b/code/renderergl2/tr_bsp.c @@ -276,7 +276,7 @@ static void R_LoadLightmaps( lump_t *l, lump_t *surfs ) { tr.deluxemaps = ri.Hunk_Alloc( tr.numLightmaps * sizeof(image_t *), h_low ); textureInternalFormat = GL_RGBA8; - if (r_hdr->integer) + if (r_hdr->integer && !qglesMajorVersion) { // Check for the first hdr lightmap, if it exists, use GL_RGBA16 for textures. char filename[MAX_QPATH]; diff --git a/code/renderergl2/tr_cmds.c b/code/renderergl2/tr_cmds.c index 38ba45d2..2025c41d 100644 --- a/code/renderergl2/tr_cmds.c +++ b/code/renderergl2/tr_cmds.c @@ -348,7 +348,13 @@ void RE_BeginFrame( stereoFrame_t stereoFrame ) { // if ( r_measureOverdraw->integer ) { - if ( glConfig.stencilBits < 4 ) + if ( qglesMajorVersion >= 1 && !glRefConfig.readStencil ) + { + ri.Printf( PRINT_WARNING, "OpenGL ES needs GL_NV_read_stencil to read stencil bits to measure overdraw\n" ); + ri.Cvar_Set( "r_measureOverdraw", "0" ); + r_measureOverdraw->modified = qfalse; + } + else if ( glConfig.stencilBits < 4 ) { ri.Printf( PRINT_ALL, "Warning: not enough stencil bits to measure overdraw: %d\n", glConfig.stencilBits ); ri.Cvar_Set( "r_measureOverdraw", "0" ); @@ -426,6 +432,13 @@ void RE_BeginFrame( stereoFrame_t stereoFrame ) { } else { + if (qglesMajorVersion >= 1 && r_anaglyphMode->integer) + { + ri.Printf( PRINT_WARNING, "OpenGL ES does not support drawing to separate buffer for anaglyph mode\n" ); + ri.Cvar_Set( "r_anaglyphMode", "0" ); + r_anaglyphMode->modified = qfalse; + } + if(r_anaglyphMode->integer) { if(r_anaglyphMode->modified) diff --git a/code/renderergl2/tr_extensions.c b/code/renderergl2/tr_extensions.c index ebc985a9..f0ea7d0a 100644 --- a/code/renderergl2/tr_extensions.c +++ b/code/renderergl2/tr_extensions.c @@ -53,8 +53,83 @@ void GLimp_InitExtraExtensions(void) // GL function loader, based on https://gist.github.com/rygorous/16796a0c876cf8a5f542caddb55bce8a #define GLE(ret, name, ...) qgl##name = (name##proc *) SDL_GL_GetProcAddress("gl" #name); + // + // OpenGL ES extensions + // + if (qglesMajorVersion) + { + if (!r_allowExtensions->integer) + goto done; + + extension = "GL_EXT_occlusion_query_boolean"; + if (SDL_GL_ExtensionSupported(extension)) + { + glRefConfig.occlusionQuery = qtrue; + glRefConfig.occlusionQueryTarget = GL_ANY_SAMPLES_PASSED; + + QGL_ARB_occlusion_query_PROCS; + + ri.Printf(PRINT_ALL, result[glRefConfig.occlusionQuery], extension); + } + else + { + ri.Printf(PRINT_ALL, result[2], extension); + } + + // GL_NV_read_depth + extension = "GL_NV_read_depth"; + if (SDL_GL_ExtensionSupported(extension)) + { + glRefConfig.readDepth = qtrue; + ri.Printf(PRINT_ALL, result[glRefConfig.readDepth], extension); + } + else + { + ri.Printf(PRINT_ALL, result[2], extension); + } + + // GL_NV_read_stencil + extension = "GL_NV_read_stencil"; + if (SDL_GL_ExtensionSupported(extension)) + { + glRefConfig.readStencil = qtrue; + ri.Printf(PRINT_ALL, result[glRefConfig.readStencil], extension); + } + else + { + ri.Printf(PRINT_ALL, result[2], extension); + } + + // GL_EXT_shadow_samplers + extension = "GL_EXT_shadow_samplers"; + if (qglesMajorVersion >= 3 || SDL_GL_ExtensionSupported(extension)) + { + glRefConfig.shadowSamplers = qtrue; + ri.Printf(PRINT_ALL, result[glRefConfig.shadowSamplers], extension); + } + else + { + ri.Printf(PRINT_ALL, result[2], extension); + } + + // GL_OES_standard_derivatives + extension = "GL_OES_standard_derivatives"; + if (qglesMajorVersion >= 3 || SDL_GL_ExtensionSupported(extension)) + { + glRefConfig.standardDerivatives = qtrue; + ri.Printf(PRINT_ALL, result[glRefConfig.standardDerivatives], extension); + } + else + { + ri.Printf(PRINT_ALL, result[2], extension); + } + + goto done; + } + // OpenGL 1.5 - GL_ARB_occlusion_query glRefConfig.occlusionQuery = qtrue; + glRefConfig.occlusionQueryTarget = GL_SAMPLES_PASSED; QGL_ARB_occlusion_query_PROCS; // OpenGL 3.0 - GL_ARB_framebuffer_object @@ -146,18 +221,6 @@ void GLimp_InitExtraExtensions(void) ri.Printf(PRINT_ALL, result[2], extension); } - // Determine GLSL version - if (1) - { - char version[256]; - - Q_strncpyz(version, (char *)qglGetString(GL_SHADING_LANGUAGE_VERSION), sizeof(version)); - - sscanf(version, "%d.%d", &glRefConfig.glslMajorVersion, &glRefConfig.glslMinorVersion); - - ri.Printf(PRINT_ALL, "...using GLSL version %s\n", version); - } - glRefConfig.memInfo = MI_NONE; // GL_NVX_gpu_memory_info @@ -249,5 +312,26 @@ void GLimp_InitExtraExtensions(void) ri.Printf(PRINT_ALL, result[2], extension); } +done: + + // Determine GLSL version + if (1) + { + char version[256], *version_p; + + Q_strncpyz(version, (char *)qglGetString(GL_SHADING_LANGUAGE_VERSION), sizeof(version)); + + // Skip leading text such as "OpenGL ES GLSL ES " + version_p = version; + while ( *version_p && !isdigit( *version_p ) ) + { + version_p++; + } + + sscanf(version_p, "%d.%d", &glRefConfig.glslMajorVersion, &glRefConfig.glslMinorVersion); + + ri.Printf(PRINT_ALL, "...using GLSL version %s\n", version); + } + #undef GLE } diff --git a/code/renderergl2/tr_flares.c b/code/renderergl2/tr_flares.c index fc83df7b..edf76822 100644 --- a/code/renderergl2/tr_flares.c +++ b/code/renderergl2/tr_flares.c @@ -478,6 +478,14 @@ void RB_RenderFlares (void) { return; } + if ( r_flares->modified ) { + if ( qglesMajorVersion >= 1 && !glRefConfig.readDepth ) { + ri.Printf( PRINT_WARNING, "OpenGL ES needs GL_NV_read_depth to read depth to determine if flares are visible\n" ); + ri.Cvar_Set( "r_flares", "0" ); + } + r_flares->modified = qfalse; + } + if(r_flareCoeff->modified) { R_SetFlareCoeff(); diff --git a/code/renderergl2/tr_glsl.c b/code/renderergl2/tr_glsl.c index 2c834f10..bf93a2f4 100644 --- a/code/renderergl2/tr_glsl.c +++ b/code/renderergl2/tr_glsl.c @@ -249,11 +249,25 @@ static void GLSL_GetShaderHeader( GLenum shaderType, const GLchar *extra, char * // HACK: abuse the GLSL preprocessor to turn GLSL 1.20 shaders into 1.30 ones if(glRefConfig.glslMajorVersion > 1 || (glRefConfig.glslMajorVersion == 1 && glRefConfig.glslMinorVersion >= 30)) { - if (glRefConfig.glslMajorVersion > 1 || (glRefConfig.glslMajorVersion == 1 && glRefConfig.glslMinorVersion >= 50)) + if (qglesMajorVersion >= 3 && glRefConfig.glslMajorVersion >= 3) + Q_strcat(dest, size, "#version 300 es\n"); + else if (glRefConfig.glslMajorVersion > 1 || (glRefConfig.glslMajorVersion == 1 && glRefConfig.glslMinorVersion >= 50)) Q_strcat(dest, size, "#version 150\n"); else Q_strcat(dest, size, "#version 130\n"); + // `extra' may contain #extension which must be directly after #version + if (extra) + { + Q_strcat(dest, size, extra); + } + + if (qglesMajorVersion >= 2) + { + Q_strcat(dest, size, "precision mediump float;\n"); + Q_strcat(dest, size, "precision mediump sampler2DShadow;\n"); + } + if(shaderType == GL_VERTEX_SHADER) { Q_strcat(dest, size, "#define attribute in\n"); @@ -272,8 +286,34 @@ static void GLSL_GetShaderHeader( GLenum shaderType, const GLchar *extra, char * } else { - Q_strcat(dest, size, "#version 120\n"); - Q_strcat(dest, size, "#define shadow2D(a,b) shadow2D(a,b).r \n"); + if (qglesMajorVersion >= 2) + { + Q_strcat(dest, size, "#version 100\n"); + + if (extra) + { + Q_strcat(dest, size, extra); + } + + Q_strcat(dest, size, "precision mediump float;\n"); + + if (glRefConfig.shadowSamplers) + { + Q_strcat(dest, size, "precision mediump sampler2DShadow;\n"); + Q_strcat(dest, size, "#define shadow2D(a,b) shadow2DEXT(a,b)\n"); + } + } + else + { + Q_strcat(dest, size, "#version 120\n"); + + if (extra) + { + Q_strcat(dest, size, extra); + } + + Q_strcat(dest, size, "#define shadow2D(a,b) shadow2D(a,b).r\n"); + } } // HACK: add some macros to avoid extra uniforms and save speed and code maintenance @@ -361,11 +401,6 @@ static void GLSL_GetShaderHeader( GLenum shaderType, const GLchar *extra, char * Q_strcat(dest, size, va("#define ROUGHNESS_MIPS float(%d)\n", numRoughnessMips)); } - if (extra) - { - Q_strcat(dest, size, extra); - } - // OK we added a lot of stuff but if we do something bad in the GLSL shaders then we want the proper line // so we have to reset the line counting Q_strcat(dest, size, "#line 0\n"); @@ -1350,84 +1385,108 @@ void GLSL_InitGPUShaders(void) } - attribs = ATTR_POSITION | ATTR_TEXCOORD; - extradefines[0] = '\0'; + // GLSL 1.10+ or GL_EXT_shadow_samplers extension are required for sampler2DShadow type + if (glRefConfig.glslMajorVersion > 1 || (glRefConfig.glslMajorVersion == 1 && glRefConfig.glslMinorVersion >= 10) + || glRefConfig.shadowSamplers) + { + attribs = ATTR_POSITION | ATTR_TEXCOORD; + extradefines[0] = '\0'; - if (r_shadowFilter->integer >= 1) - Q_strcat(extradefines, 1024, "#define USE_SHADOW_FILTER\n"); + if (qglesMajorVersion < 3 && glRefConfig.shadowSamplers) + { + Q_strcat(extradefines, 1024, "#extension GL_EXT_shadow_samplers : enable\n"); + } - if (r_shadowFilter->integer >= 2) - Q_strcat(extradefines, 1024, "#define USE_SHADOW_FILTER2\n"); + if (r_shadowFilter->integer >= 1) + Q_strcat(extradefines, 1024, "#define USE_SHADOW_FILTER\n"); - if (r_shadowCascadeZFar->integer != 0) - Q_strcat(extradefines, 1024, "#define USE_SHADOW_CASCADE\n"); + if (r_shadowFilter->integer >= 2) + Q_strcat(extradefines, 1024, "#define USE_SHADOW_FILTER2\n"); - Q_strcat(extradefines, 1024, va("#define r_shadowMapSize %f\n", r_shadowMapSize->value)); - Q_strcat(extradefines, 1024, va("#define r_shadowCascadeZFar %f\n", r_shadowCascadeZFar->value)); + if (r_shadowCascadeZFar->integer != 0) + Q_strcat(extradefines, 1024, "#define USE_SHADOW_CASCADE\n"); + Q_strcat(extradefines, 1024, va("#define r_shadowMapSize %f\n", r_shadowMapSize->value)); + Q_strcat(extradefines, 1024, va("#define r_shadowCascadeZFar %f\n", r_shadowCascadeZFar->value)); - if (!GLSL_InitGPUShader(&tr.shadowmaskShader, "shadowmask", attribs, qtrue, extradefines, qtrue, fallbackShader_shadowmask_vp, fallbackShader_shadowmask_fp)) - { - ri.Error(ERR_FATAL, "Could not load shadowmask shader!"); - } + if (!GLSL_InitGPUShader(&tr.shadowmaskShader, "shadowmask", attribs, qtrue, extradefines, qtrue, fallbackShader_shadowmask_vp, fallbackShader_shadowmask_fp)) + { + ri.Error(ERR_FATAL, "Could not load shadowmask shader!"); + } - GLSL_InitUniforms(&tr.shadowmaskShader); + GLSL_InitUniforms(&tr.shadowmaskShader); - GLSL_SetUniformInt(&tr.shadowmaskShader, UNIFORM_SCREENDEPTHMAP, TB_COLORMAP); - GLSL_SetUniformInt(&tr.shadowmaskShader, UNIFORM_SHADOWMAP, TB_SHADOWMAP); - GLSL_SetUniformInt(&tr.shadowmaskShader, UNIFORM_SHADOWMAP2, TB_SHADOWMAP2); - GLSL_SetUniformInt(&tr.shadowmaskShader, UNIFORM_SHADOWMAP3, TB_SHADOWMAP3); - GLSL_SetUniformInt(&tr.shadowmaskShader, UNIFORM_SHADOWMAP4, TB_SHADOWMAP4); + GLSL_SetUniformInt(&tr.shadowmaskShader, UNIFORM_SCREENDEPTHMAP, TB_COLORMAP); + GLSL_SetUniformInt(&tr.shadowmaskShader, UNIFORM_SHADOWMAP, TB_SHADOWMAP); + GLSL_SetUniformInt(&tr.shadowmaskShader, UNIFORM_SHADOWMAP2, TB_SHADOWMAP2); + GLSL_SetUniformInt(&tr.shadowmaskShader, UNIFORM_SHADOWMAP3, TB_SHADOWMAP3); + GLSL_SetUniformInt(&tr.shadowmaskShader, UNIFORM_SHADOWMAP4, TB_SHADOWMAP4); - GLSL_FinishGPUShader(&tr.shadowmaskShader); - - numEtcShaders++; + GLSL_FinishGPUShader(&tr.shadowmaskShader); + numEtcShaders++; + } - attribs = ATTR_POSITION | ATTR_TEXCOORD; - extradefines[0] = '\0'; - if (!GLSL_InitGPUShader(&tr.ssaoShader, "ssao", attribs, qtrue, extradefines, qtrue, fallbackShader_ssao_vp, fallbackShader_ssao_fp)) + // GLSL 1.10+ or GL_OES_standard_derivatives extension are required for dFdx() and dFdy() GLSL functions + if (glRefConfig.glslMajorVersion > 1 || (glRefConfig.glslMajorVersion == 1 && glRefConfig.glslMinorVersion >= 10) + || glRefConfig.standardDerivatives) { - ri.Error(ERR_FATAL, "Could not load ssao shader!"); - } + attribs = ATTR_POSITION | ATTR_TEXCOORD; + extradefines[0] = '\0'; - GLSL_InitUniforms(&tr.ssaoShader); + if (qglesMajorVersion < 3 && glRefConfig.standardDerivatives) + { + Q_strcat(extradefines, 1024, "#extension GL_OES_standard_derivatives : enable\n"); + } - GLSL_SetUniformInt(&tr.ssaoShader, UNIFORM_SCREENDEPTHMAP, TB_COLORMAP); + if (!GLSL_InitGPUShader(&tr.ssaoShader, "ssao", attribs, qtrue, extradefines, qtrue, fallbackShader_ssao_vp, fallbackShader_ssao_fp)) + { + ri.Error(ERR_FATAL, "Could not load ssao shader!"); + } - GLSL_FinishGPUShader(&tr.ssaoShader); + GLSL_InitUniforms(&tr.ssaoShader); - numEtcShaders++; + GLSL_SetUniformInt(&tr.ssaoShader, UNIFORM_SCREENDEPTHMAP, TB_COLORMAP); + GLSL_FinishGPUShader(&tr.ssaoShader); - for (i = 0; i < 4; i++) - { - attribs = ATTR_POSITION | ATTR_TEXCOORD; - extradefines[0] = '\0'; + numEtcShaders++; - if (i & 1) - Q_strcat(extradefines, 1024, "#define USE_VERTICAL_BLUR\n"); - else - Q_strcat(extradefines, 1024, "#define USE_HORIZONTAL_BLUR\n"); - if (!(i & 2)) - Q_strcat(extradefines, 1024, "#define USE_DEPTH\n"); + for (i = 0; i < 4; i++) + { + attribs = ATTR_POSITION | ATTR_TEXCOORD; + extradefines[0] = '\0'; + if (qglesMajorVersion < 3 && glRefConfig.standardDerivatives) + { + Q_strcat(extradefines, 1024, "#extension GL_OES_standard_derivatives : enable\n"); + } - if (!GLSL_InitGPUShader(&tr.depthBlurShader[i], "depthBlur", attribs, qtrue, extradefines, qtrue, fallbackShader_depthblur_vp, fallbackShader_depthblur_fp)) - { - ri.Error(ERR_FATAL, "Could not load depthBlur shader!"); - } + if (i & 1) + Q_strcat(extradefines, 1024, "#define USE_VERTICAL_BLUR\n"); + else + Q_strcat(extradefines, 1024, "#define USE_HORIZONTAL_BLUR\n"); + + if (!(i & 2)) + Q_strcat(extradefines, 1024, "#define USE_DEPTH\n"); + + + if (!GLSL_InitGPUShader(&tr.depthBlurShader[i], "depthBlur", attribs, qtrue, extradefines, qtrue, fallbackShader_depthblur_vp, fallbackShader_depthblur_fp)) + { + ri.Error(ERR_FATAL, "Could not load depthBlur shader!"); + } - GLSL_InitUniforms(&tr.depthBlurShader[i]); + GLSL_InitUniforms(&tr.depthBlurShader[i]); - GLSL_SetUniformInt(&tr.depthBlurShader[i], UNIFORM_SCREENIMAGEMAP, TB_COLORMAP); - GLSL_SetUniformInt(&tr.depthBlurShader[i], UNIFORM_SCREENDEPTHMAP, TB_LIGHTMAP); + GLSL_SetUniformInt(&tr.depthBlurShader[i], UNIFORM_SCREENIMAGEMAP, TB_COLORMAP); + GLSL_SetUniformInt(&tr.depthBlurShader[i], UNIFORM_SCREENDEPTHMAP, TB_LIGHTMAP); - GLSL_FinishGPUShader(&tr.depthBlurShader[i]); + GLSL_FinishGPUShader(&tr.depthBlurShader[i]); - numEtcShaders++; + numEtcShaders++; + } } #if 0 diff --git a/code/renderergl2/tr_image.c b/code/renderergl2/tr_image.c index 101513b6..0e829d51 100644 --- a/code/renderergl2/tr_image.c +++ b/code/renderergl2/tr_image.c @@ -1455,6 +1455,106 @@ byte mipBlendColors[16][4] = { {0,0,255,128}, }; +/* +================== +R_ConvertTextureFormat + +Convert RGBA unsigned byte to specified format and type +================== +*/ +#define ROW_PADDING( width, bpp, alignment ) PAD( (width) * (bpp), (alignment) ) - (width) * (bpp) +void R_ConvertTextureFormat( const byte *in, int width, int height, GLenum format, GLenum type, byte *out ) +{ + int x, y, rowPadding; + int unpackAlign = 4; // matches GL_UNPACK_ALIGNMENT default + + if ( format == GL_RGB && type == GL_UNSIGNED_BYTE ) + { + rowPadding = ROW_PADDING( width, 3, unpackAlign ); + + for ( y = 0; y < height; y++ ) + { + for ( x = 0; x < width; x++ ) + { + *out++ = *in++; + *out++ = *in++; + *out++ = *in++; + in++; + } + + out += rowPadding; + } + } + else if ( format == GL_LUMINANCE && type == GL_UNSIGNED_BYTE ) + { + rowPadding = ROW_PADDING( width, 1, unpackAlign ); + + for ( y = 0; y < height; y++ ) + { + for ( x = 0; x < width; x++ ) + { + *out++ = *in++; // red + in += 3; + } + + out += rowPadding; + } + } + else if ( format == GL_LUMINANCE_ALPHA && type == GL_UNSIGNED_BYTE ) + { + rowPadding = ROW_PADDING( width, 2, unpackAlign ); + + for ( y = 0; y < height; y++ ) + { + for ( x = 0; x < width; x++ ) + { + *out++ = *in++; // red + in += 2; + *out++ = *in++; // alpha + } + + out += rowPadding; + } + } + else if ( format == GL_RGB && type == GL_UNSIGNED_SHORT_5_6_5 ) + { + rowPadding = ROW_PADDING( width, 2, unpackAlign ); + + for ( y = 0; y < height; y++ ) + { + for ( x = 0; x < width; x++, in += 4, out += 2 ) + { + *((unsigned short*)out) = ( (unsigned short)( in[0] >> 3 ) << 11 ) + | ( (unsigned short)( in[1] >> 2 ) << 5 ) + | ( (unsigned short)( in[2] >> 3 ) << 0 ); + } + + out += rowPadding; + } + } + else if ( format == GL_RGBA && type == GL_UNSIGNED_SHORT_4_4_4_4 ) + { + rowPadding = ROW_PADDING( width, 2, unpackAlign ); + + for ( y = 0; y < height; y++ ) + { + for ( x = 0; x < width; x++, in += 4, out += 2 ) + { + *((unsigned short*)out) = ( (unsigned short)( in[0] >> 4 ) << 12 ) + | ( (unsigned short)( in[1] >> 4 ) << 8 ) + | ( (unsigned short)( in[2] >> 4 ) << 4 ) + | ( (unsigned short)( in[3] >> 4 ) << 0 ); + } + + out += rowPadding; + } + } + else + { + ri.Error( ERR_DROP, "Unable to convert RGBA image to OpenGL format 0x%X and type 0x%X", format, type ); + } +} + static void RawImage_SwizzleRA( byte *data, int width, int height ) { int i; @@ -1944,18 +2044,20 @@ static GLenum PixelDataFormatFromInternalFormat(GLenum internalFormat) } } -static void RawImage_UploadTexture(GLuint texture, byte *data, int x, int y, int width, int height, GLenum target, GLenum picFormat, int numMips, GLenum internalFormat, imgType_t type, imgFlags_t flags, qboolean subtexture ) +static void RawImage_UploadTexture(GLuint texture, byte *data, int x, int y, int width, int height, GLenum target, GLenum picFormat, GLenum dataFormat, GLenum dataType, int numMips, GLenum internalFormat, imgType_t type, imgFlags_t flags, qboolean subtexture ) { - GLenum dataFormat, dataType; qboolean rgtc = internalFormat == GL_COMPRESSED_RG_RGTC2; qboolean rgba8 = picFormat == GL_RGBA8 || picFormat == GL_SRGB8_ALPHA8_EXT; qboolean rgba = rgba8 || picFormat == GL_RGBA16; qboolean mipmap = !!(flags & IMGFLAG_MIPMAP); int size, miplevel; qboolean lastMip = qfalse; + byte *formatBuffer = NULL; - dataFormat = PixelDataFormatFromInternalFormat(internalFormat); - dataType = picFormat == GL_RGBA16 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE; + if (qglesMajorVersion && rgba8 && (dataFormat != GL_RGBA || dataType != GL_UNSIGNED_BYTE)) + { + formatBuffer = ri.Hunk_AllocateTempMemory(4 * width * height); + } miplevel = 0; do @@ -1974,6 +2076,11 @@ static void RawImage_UploadTexture(GLuint texture, byte *data, int x, int y, int if (rgba8 && rgtc) RawImage_UploadToRgtc2Texture(texture, miplevel, x, y, width, height, data); + else if (formatBuffer) + { + R_ConvertTextureFormat(data, width, height, dataFormat, dataType, formatBuffer); + qglTextureSubImage2DEXT(texture, target, miplevel, x, y, width, height, dataFormat, dataType, formatBuffer); + } else qglTextureSubImage2DEXT(texture, target, miplevel, x, y, width, height, dataFormat, dataType, data); } @@ -2007,6 +2114,9 @@ static void RawImage_UploadTexture(GLuint texture, byte *data, int x, int y, int } } while (!lastMip); + + if (formatBuffer != NULL) + ri.Hunk_FreeTempMemory(formatBuffer); } @@ -2016,7 +2126,7 @@ Upload32 =============== */ -static void Upload32(byte *data, int x, int y, int width, int height, GLenum picFormat, int numMips, image_t *image, qboolean scaled) +static void Upload32(byte *data, int x, int y, int width, int height, GLenum picFormat, GLenum dataFormat, GLenum dataType, int numMips, image_t *image, qboolean scaled) { int i, c; byte *scan; @@ -2071,7 +2181,7 @@ static void Upload32(byte *data, int x, int y, int width, int height, GLenum pic for (i = 0; i < 6; i++) { int w2 = width, h2 = height; - RawImage_UploadTexture(image->texnum, data, x, y, width, height, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, picFormat, numMips, internalFormat, type, flags, qfalse); + RawImage_UploadTexture(image->texnum, data, x, y, width, height, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, picFormat, dataFormat, dataType, numMips, internalFormat, type, flags, qfalse); for (c = numMips; c; c--) { data += CalculateMipSize(w2, h2, picFormat); @@ -2082,7 +2192,7 @@ static void Upload32(byte *data, int x, int y, int width, int height, GLenum pic } else { - RawImage_UploadTexture(image->texnum, data, x, y, width, height, GL_TEXTURE_2D, picFormat, numMips, internalFormat, type, flags, qfalse); + RawImage_UploadTexture(image->texnum, data, x, y, width, height, GL_TEXTURE_2D, picFormat, dataFormat, dataType, numMips, internalFormat, type, flags, qfalse); } GL_CheckErrors(); @@ -2108,7 +2218,7 @@ image_t *R_CreateImage2( const char *name, byte *pic, int width, int height, GLe qboolean picmip = !!(flags & IMGFLAG_PICMIP); qboolean lastMip; GLenum textureTarget = cubemap ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D; - GLenum dataFormat; + GLenum dataFormat, dataType; if (strlen(name) >= MAX_QPATH ) { ri.Error (ERR_DROP, "R_CreateImage: \"%s\" is too long", name); @@ -2140,6 +2250,53 @@ image_t *R_CreateImage2( const char *name, byte *pic, int width, int height, GLe if (!internalFormat) internalFormat = RawImage_GetFormat(pic, width * height, picFormat, isLightmap, image->type, image->flags); + dataFormat = PixelDataFormatFromInternalFormat(internalFormat); + dataType = picFormat == GL_RGBA16 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE; + + // Convert image data format for OpenGL ES, data is converted for each mip level + if (qglesMajorVersion) + { + switch (internalFormat) + { + case GL_LUMINANCE: + case GL_LUMINANCE8: + internalFormat = GL_LUMINANCE; + dataFormat = GL_LUMINANCE; + dataType = GL_UNSIGNED_BYTE; + break; + case GL_LUMINANCE_ALPHA: + case GL_LUMINANCE8_ALPHA8: + internalFormat = GL_LUMINANCE_ALPHA; + dataFormat = GL_LUMINANCE_ALPHA; + dataType = GL_UNSIGNED_BYTE; + break; + case GL_RGB: + case GL_RGB8: + internalFormat = GL_RGB; + dataFormat = GL_RGB; + dataType = GL_UNSIGNED_BYTE; + break; + case GL_RGB5: + internalFormat = GL_RGB; + dataFormat = GL_RGB; + dataType = GL_UNSIGNED_SHORT_5_6_5; + break; + case GL_RGBA: + case GL_RGBA8: + internalFormat = GL_RGBA; + dataFormat = GL_RGBA; + dataType = GL_UNSIGNED_BYTE; + break; + case GL_RGBA4: + internalFormat = GL_RGBA; + dataFormat = GL_RGBA; + dataType = GL_UNSIGNED_SHORT_4_4_4_4; + break; + default: + ri.Error( ERR_DROP, "Missing OpenGL ES support for image '%s' with internal format 0x%X\n", name, internalFormat ); + } + } + image->internalFormat = internalFormat; // Possibly scale image before uploading. @@ -2164,7 +2321,6 @@ image_t *R_CreateImage2( const char *name, byte *pic, int width, int height, GLe image->uploadHeight = height; // Allocate texture storage so we don't have to worry about it later. - dataFormat = PixelDataFormatFromInternalFormat(internalFormat); mipWidth = width; mipHeight = height; miplevel = 0; @@ -2176,11 +2332,11 @@ image_t *R_CreateImage2( const char *name, byte *pic, int width, int height, GLe int i; for (i = 0; i < 6; i++) - qglTextureImage2DEXT(image->texnum, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, miplevel, internalFormat, mipWidth, mipHeight, 0, dataFormat, GL_UNSIGNED_BYTE, NULL); + qglTextureImage2DEXT(image->texnum, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, miplevel, internalFormat, mipWidth, mipHeight, 0, dataFormat, dataType, NULL); } else { - qglTextureImage2DEXT(image->texnum, GL_TEXTURE_2D, miplevel, internalFormat, mipWidth, mipHeight, 0, dataFormat, GL_UNSIGNED_BYTE, NULL); + qglTextureImage2DEXT(image->texnum, GL_TEXTURE_2D, miplevel, internalFormat, mipWidth, mipHeight, 0, dataFormat, dataType, NULL); } mipWidth = MAX(1, mipWidth >> 1); @@ -2191,7 +2347,7 @@ image_t *R_CreateImage2( const char *name, byte *pic, int width, int height, GLe // Upload data. if (pic) - Upload32(pic, 0, 0, width, height, picFormat, numMips, image, scaled); + Upload32(pic, 0, 0, width, height, picFormat, dataFormat, dataType, numMips, image, scaled); if (resampledBuffer != NULL) ri.Hunk_FreeTempMemory(resampledBuffer); @@ -2252,7 +2408,13 @@ image_t *R_CreateImage(const char *name, byte *pic, int width, int height, imgTy void R_UpdateSubImage( image_t *image, byte *pic, int x, int y, int width, int height, GLenum picFormat ) { - Upload32(pic, x, y, width, height, picFormat, 0, image, qfalse); + GLenum dataFormat, dataType; + + // TODO: This is fine for lightmaps but (unused) general RGBA images need to store dataFormat / dataType in image_t for OpenGL ES? + dataFormat = PixelDataFormatFromInternalFormat(image->internalFormat); + dataType = picFormat == GL_RGBA16 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE; + + Upload32(pic, x, y, width, height, picFormat, dataFormat, dataType, 0, image, qfalse); } //=================================================================== diff --git a/code/renderergl2/tr_init.c b/code/renderergl2/tr_init.c index 0edfe6be..12570392 100644 --- a/code/renderergl2/tr_init.c +++ b/code/renderergl2/tr_init.c @@ -451,21 +451,43 @@ Return value must be freed with ri.Hunk_FreeTempMemory() byte *RB_ReadPixels(int x, int y, int width, int height, size_t *offset, int *padlen) { byte *buffer, *bufstart; - int padwidth, linelen; - GLint packAlign; - + int padwidth, linelen, bytesPerPixel; + int yin, xin, xout; + GLint packAlign, format; + + // OpenGL ES is only required to support reading GL_RGBA + if (qglesMajorVersion >= 1) { + format = GL_RGBA; + bytesPerPixel = 4; + } else { + format = GL_RGB; + bytesPerPixel = 3; + } + qglGetIntegerv(GL_PACK_ALIGNMENT, &packAlign); - - linelen = width * 3; + + linelen = width * bytesPerPixel; padwidth = PAD(linelen, packAlign); - + // Allocate a few more bytes so that we can choose an alignment we like buffer = ri.Hunk_AllocateTempMemory(padwidth * height + *offset + packAlign - 1); - + bufstart = PADP((intptr_t) buffer + *offset, packAlign); + qglReadPixels(x, y, width, height, format, GL_UNSIGNED_BYTE, bufstart); + + linelen = width * 3; + + // Convert RGBA to RGB, in place, line by line + if (format == GL_RGBA) { + for (yin = 0; yin < height; yin++) { + for (xin = 0, xout = 0; xout < linelen; xin += 4, xout += 3) { + bufstart[yin*padwidth + xout + 0] = bufstart[yin*padwidth + xin + 0]; + bufstart[yin*padwidth + xout + 1] = bufstart[yin*padwidth + xin + 1]; + bufstart[yin*padwidth + xout + 2] = bufstart[yin*padwidth + xin + 2]; + } + } + } - qglReadPixels(x, y, width, height, GL_RGB, GL_UNSIGNED_BYTE, bufstart); - *offset = bufstart - buffer; *padlen = padwidth - linelen; @@ -877,9 +899,10 @@ const void *RB_TakeVideoFrameCmd( const void *data ) { const videoFrameCommand_t *cmd; byte *cBuf; - size_t memcount, linelen; + size_t memcount, bytesPerPixel, linelen, avilinelen; int padwidth, avipadwidth, padlen, avipadlen; - GLint packAlign; + int yin, xin, xout; + GLint packAlign, format; // finish any 2D drawing if needed if(tess.numIndexes) @@ -887,20 +910,32 @@ const void *RB_TakeVideoFrameCmd( const void *data ) cmd = (const videoFrameCommand_t *)data; + // OpenGL ES is only required to support reading GL_RGBA + if (qglesMajorVersion >= 1) { + format = GL_RGBA; + bytesPerPixel = 4; + } else { + format = GL_RGB; + bytesPerPixel = 3; + } + qglGetIntegerv(GL_PACK_ALIGNMENT, &packAlign); - linelen = cmd->width * 3; + linelen = cmd->width * bytesPerPixel; // Alignment stuff for glReadPixels padwidth = PAD(linelen, packAlign); padlen = padwidth - linelen; + + avilinelen = cmd->width * 3; + // AVI line padding - avipadwidth = PAD(linelen, AVI_LINE_PADDING); - avipadlen = avipadwidth - linelen; + avipadwidth = PAD(avilinelen, AVI_LINE_PADDING); + avipadlen = avipadwidth - avilinelen; cBuf = PADP(cmd->captureBuffer, packAlign); - qglReadPixels(0, 0, cmd->width, cmd->height, GL_RGB, + qglReadPixels(0, 0, cmd->width, cmd->height, format, GL_UNSIGNED_BYTE, cBuf); memcount = padwidth * cmd->height; @@ -911,7 +946,21 @@ const void *RB_TakeVideoFrameCmd( const void *data ) if(cmd->motionJpeg) { - memcount = RE_SaveJPGToBuffer(cmd->encodeBuffer, linelen * cmd->height, + // Convert RGBA to RGB, in place, line by line + if (format == GL_RGBA) { + linelen = cmd->width * 3; + padlen = padwidth - linelen; + + for (yin = 0; yin < cmd->height; yin++) { + for (xin = 0, xout = 0; xout < linelen; xin += 4, xout += 3) { + cBuf[yin*padwidth + xout + 0] = cBuf[yin*padwidth + xin + 0]; + cBuf[yin*padwidth + xout + 1] = cBuf[yin*padwidth + xin + 1]; + cBuf[yin*padwidth + xout + 2] = cBuf[yin*padwidth + xin + 2]; + } + } + } + + memcount = RE_SaveJPGToBuffer(cmd->encodeBuffer, avilinelen * cmd->height, r_aviMotionJpegQuality->integer, cmd->width, cmd->height, cBuf, padlen); ri.CL_WriteAVIVideoFrame(cmd->encodeBuffer, memcount); @@ -934,7 +983,7 @@ const void *RB_TakeVideoFrameCmd( const void *data ) *destptr++ = srcptr[2]; *destptr++ = srcptr[1]; *destptr++ = srcptr[0]; - srcptr += 3; + srcptr += bytesPerPixel; } Com_Memset(destptr, '\0', avipadlen); diff --git a/code/renderergl2/tr_local.h b/code/renderergl2/tr_local.h index 6a0aa36f..bdc6e477 100644 --- a/code/renderergl2/tr_local.h +++ b/code/renderergl2/tr_local.h @@ -1406,6 +1406,7 @@ typedef struct { qboolean intelGraphics; qboolean occlusionQuery; + GLenum occlusionQueryTarget; int glslMajorVersion; int glslMinorVersion; @@ -1429,6 +1430,12 @@ typedef struct { qboolean vertexArrayObject; qboolean directStateAccess; + + // OpenGL ES extensions + qboolean readDepth; + qboolean readStencil; + qboolean shadowSamplers; + qboolean standardDerivatives; } glRefConfig_t; @@ -2502,5 +2509,7 @@ size_t RE_SaveJPGToBuffer(byte *buffer, size_t bufSize, int quality, void RE_TakeVideoFrame( int width, int height, byte *captureBuffer, byte *encodeBuffer, qboolean motionJpeg ); +void R_ConvertTextureFormat( const byte *in, int width, int height, GLenum format, GLenum type, byte *out ); + #endif //TR_LOCAL_H diff --git a/code/renderergl2/tr_postprocess.c b/code/renderergl2/tr_postprocess.c index 3d71f304..ff9a25a1 100644 --- a/code/renderergl2/tr_postprocess.c +++ b/code/renderergl2/tr_postprocess.c @@ -290,6 +290,8 @@ static qboolean RB_UpdateSunFlareVis(void) ri.Printf(PRINT_DEVELOPER, "Waited %d iterations\n", iter); } + // Note: On desktop OpenGL this is a sample count (glRefConfig.occlusionQueryTarget == GL_SAMPLES_PASSED) + // but on OpenGL ES this is a boolean (glRefConfig.occlusionQueryTarget == GL_ANY_SAMPLES_PASSED) qglGetQueryObjectuiv(tr.sunFlareQuery[tr.sunFlareQueryIndex], GL_QUERY_RESULT, &sampleCount); return sampleCount > 0; } diff --git a/code/sdl/sdl_glimp.c b/code/sdl/sdl_glimp.c index 30986ab1..b24759b5 100644 --- a/code/sdl/sdl_glimp.c +++ b/code/sdl/sdl_glimp.c @@ -52,6 +52,7 @@ cvar_t *r_allowSoftwareGL; // Don't abort out if a hardware visual can't be obta cvar_t *r_allowResize; // make window resizable cvar_t *r_centerWindow; cvar_t *r_sdlDriver; +cvar_t *r_useOpenGLES; int qglMajorVersion, qglMinorVersion; int qglesMajorVersion, qglesMinorVersion; @@ -230,6 +231,27 @@ static void GLimp_DetectAvailableModes(void) SDL_free( modes ); } +/* +=============== +OpenGL ES compatibility +=============== +*/ +static void APIENTRY GLimp_GLES_ClearDepth( GLclampd depth ) { + qglClearDepthf( depth ); +} + +static void APIENTRY GLimp_GLES_DepthRange( GLclampd near_val, GLclampd far_val ) { + qglDepthRangef( near_val, far_val ); +} + +static void APIENTRY GLimp_GLES_DrawBuffer( GLenum mode ) { + // unsupported +} + +static void APIENTRY GLimp_GLES_PolygonMode( GLenum face, GLenum mode ) { + // unsupported +} + /* =============== GLimp_GetProcAddresses @@ -306,8 +328,11 @@ static qboolean GLimp_GetProcAddresses( qboolean fixedFunction ) { QGL_1_3_PROCS; QGL_1_5_PROCS; QGL_2_0_PROCS; - // error so this doesn't segfault due to NULL desktop GL functions being used - Com_Error( ERR_FATAL, "Unsupported OpenGL Version: %s", version ); + + qglClearDepth = GLimp_GLES_ClearDepth; + qglDepthRange = GLimp_GLES_DepthRange; + qglDrawBuffer = GLimp_GLES_DrawBuffer; + qglPolygonMode = GLimp_GLES_PolygonMode; } else { Com_Error( ERR_FATAL, "Unsupported OpenGL Version (%s), OpenGL 2.0 is required", version ); } @@ -633,57 +658,91 @@ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qbool if (!fixedFunction) { - int profileMask, majorVersion, minorVersion; - SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &profileMask); - SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &majorVersion); - SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, &minorVersion); - - ri.Printf(PRINT_ALL, "Trying to get an OpenGL 3.2 core context\n"); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); - if ((SDL_glContext = SDL_GL_CreateContext(SDL_window)) == NULL) - { - ri.Printf(PRINT_ALL, "SDL_GL_CreateContext failed: %s\n", SDL_GetError()); - ri.Printf(PRINT_ALL, "Reverting to default context\n"); + int profileMask; - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, profileMask); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, majorVersion); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, minorVersion); - } - else - { - const char *renderer; + SDL_GL_GetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, &profileMask ); - ri.Printf(PRINT_ALL, "SDL_GL_CreateContext succeeded.\n"); + if ( r_useOpenGLES->integer == 1 || ( r_useOpenGLES->integer == -1 && profileMask == SDL_GL_CONTEXT_PROFILE_ES ) ) + { + ri.Printf( PRINT_ALL, "Trying to get an OpenGL ES 2.0 context\n" ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 2 ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 0 ); - if ( GLimp_GetProcAddresses( fixedFunction ) ) + SDL_glContext = SDL_GL_CreateContext( SDL_window ); + if ( !SDL_glContext ) { - renderer = (const char *)qglGetString(GL_RENDERER); + ri.Printf( PRINT_ALL, "SDL_GL_CreateContext failed: %s\n", SDL_GetError() ); } else { - ri.Printf( PRINT_ALL, "GLimp_GetProcAddresses() failed for OpenGL 3.2 core context\n" ); - renderer = NULL; + ri.Printf( PRINT_ALL, "SDL_GL_CreateContext succeeded.\n" ); + + if ( !GLimp_GetProcAddresses( fixedFunction ) ) + { + ri.Printf( PRINT_ALL, "GLimp_GetProcAddresses() failed for OpenGL ES 2.0 context\n" ); + GLimp_ClearProcAddresses(); + SDL_GL_DeleteContext( SDL_glContext ); + SDL_glContext = NULL; + } } + } + + if ( !SDL_glContext ) + { + ri.Printf( PRINT_ALL, "Trying to get an OpenGL 3.2 core context\n" ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 3 ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 2 ); - if (!renderer || (strstr(renderer, "Software Renderer") || strstr(renderer, "Software Rasterizer"))) + SDL_glContext = SDL_GL_CreateContext( SDL_window ); + if ( !SDL_glContext ) { - if ( renderer ) - ri.Printf(PRINT_ALL, "GL_RENDERER is %s, rejecting context\n", renderer); + ri.Printf( PRINT_ALL, "SDL_GL_CreateContext failed: %s\n", SDL_GetError() ); + } + else + { + const char *renderer; - GLimp_ClearProcAddresses(); - SDL_GL_DeleteContext(SDL_glContext); - SDL_glContext = NULL; + ri.Printf( PRINT_ALL, "SDL_GL_CreateContext succeeded.\n" ); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, profileMask); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, majorVersion); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, minorVersion); + if ( GLimp_GetProcAddresses( fixedFunction ) ) + { + renderer = (const char *)qglGetString( GL_RENDERER ); + } + else + { + ri.Printf( PRINT_ALL, "GLimp_GetProcAddresses() failed for OpenGL 3.2 core context\n" ); + renderer = NULL; + } + + if ( !renderer || strstr( renderer, "Software Renderer" ) || strstr( renderer, "Software Rasterizer" ) ) + { + if ( renderer ) + ri.Printf(PRINT_ALL, "GL_RENDERER is %s, rejecting context\n", renderer); + + GLimp_ClearProcAddresses(); + SDL_GL_DeleteContext( SDL_glContext ); + SDL_glContext = NULL; + } } } + + if ( !SDL_glContext ) + { + ri.Printf( PRINT_ALL, "Trying to get an OpenGL 2.0 context\n" ); + + SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, 0 ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 2 ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 0 ); + } } else { + SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, 0 ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 1 ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 1 ); + SDL_glContext = NULL; } @@ -815,7 +874,7 @@ static void GLimp_InitExtensions( qboolean fixedFunction ) glConfig.textureCompression = TC_NONE; // GL_EXT_texture_compression_s3tc - if ( SDL_GL_ExtensionSupported( "GL_ARB_texture_compression" ) && + if ( ( QGLES_VERSION_ATLEAST( 2, 0 ) || SDL_GL_ExtensionSupported( "GL_ARB_texture_compression" ) ) && SDL_GL_ExtensionSupported( "GL_EXT_texture_compression_s3tc" ) ) { if ( r_ext_compressed_textures->value ) @@ -996,6 +1055,7 @@ void GLimp_Init( qboolean fixedFunction ) r_sdlDriver = ri.Cvar_Get( "r_sdlDriver", "", CVAR_ROM ); r_allowResize = ri.Cvar_Get( "r_allowResize", "0", CVAR_ARCHIVE | CVAR_LATCH ); r_centerWindow = ri.Cvar_Get( "r_centerWindow", "0", CVAR_ARCHIVE | CVAR_LATCH ); + r_useOpenGLES = ri.Cvar_Get( "r_useOpenGLES", "-1", CVAR_ARCHIVE | CVAR_LATCH ); if( ri.Cvar_VariableIntegerValue( "com_abnormalExit" ) ) { diff --git a/opengl2-readme.md b/opengl2-readme.md index ee85c02c..2e076cfa 100644 --- a/opengl2-readme.md +++ b/opengl2-readme.md @@ -63,6 +63,14 @@ For Win32: CVARS ------------------------------------------------------------------------------- +Cvars for API: + +* `r_useOpenGLES` - This enables using OpenGL ES 2+. + Many features are not supported such as sun shadows and HDR. + 1 - Use OpenGL ES. + 0 - Use desktop OpenGL. + -1 - Automatically pick (default). + Cvars for simple rendering features: * `r_ext_compressed_textures` - Automatically compress textures. From b25a3c6e4d1af0767559a030cbd12e5f942e08a7 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Wed, 5 Jun 2024 21:34:36 -0500 Subject: [PATCH 06/34] OpenGL2: Add run-time support for unsigned short indexes OpenGL ES 2 is only required to support unsigned short for indexes. --- code/renderergl2/tr_extensions.c | 24 ++++++++++++ code/renderergl2/tr_local.h | 10 ++++- code/renderergl2/tr_shade.c | 11 +++++- code/renderergl2/tr_vbo.c | 67 ++++++++++++++++++++++---------- 4 files changed, 88 insertions(+), 24 deletions(-) diff --git a/code/renderergl2/tr_extensions.c b/code/renderergl2/tr_extensions.c index f0ea7d0a..8603794b 100644 --- a/code/renderergl2/tr_extensions.c +++ b/code/renderergl2/tr_extensions.c @@ -45,6 +45,17 @@ void GLimp_InitExtraExtensions(void) if (strstr((char *)qglGetString(GL_RENDERER), "Intel")) glRefConfig.intelGraphics = qtrue; + if (qglesMajorVersion) + { + glRefConfig.vaoCacheGlIndexType = GL_UNSIGNED_SHORT; + glRefConfig.vaoCacheGlIndexSize = sizeof(unsigned short); + } + else + { + glRefConfig.vaoCacheGlIndexType = GL_UNSIGNED_INT; + glRefConfig.vaoCacheGlIndexSize = sizeof(unsigned int); + } + // set DSA fallbacks #define GLE(ret, name, ...) qgl##name = GLDSA_##name; QGL_EXT_direct_state_access_PROCS; @@ -124,6 +135,19 @@ void GLimp_InitExtraExtensions(void) ri.Printf(PRINT_ALL, result[2], extension); } + // GL_OES_element_index_uint + extension = "GL_OES_element_index_uint"; + if (SDL_GL_ExtensionSupported(extension)) + { + glRefConfig.vaoCacheGlIndexType = GL_UNSIGNED_INT; + glRefConfig.vaoCacheGlIndexSize = sizeof(unsigned int); + ri.Printf(PRINT_ALL, result[1], extension); + } + else + { + ri.Printf(PRINT_ALL, result[2], extension); + } + goto done; } diff --git a/code/renderergl2/tr_local.h b/code/renderergl2/tr_local.h index bdc6e477..eab069e1 100644 --- a/code/renderergl2/tr_local.h +++ b/code/renderergl2/tr_local.h @@ -49,8 +49,10 @@ QGL_ARB_vertex_array_object_PROCS; QGL_EXT_direct_state_access_PROCS; #undef GLE -#define GL_INDEX_TYPE GL_UNSIGNED_INT -typedef unsigned int glIndex_t; +#define GL_INDEX_TYPE GL_UNSIGNED_SHORT +typedef unsigned short glIndex_t; + +typedef unsigned int vaoCacheGlIndex_t; #define BUFFER_OFFSET(i) ((char *)NULL + (i)) @@ -1431,6 +1433,9 @@ typedef struct { qboolean vertexArrayObject; qboolean directStateAccess; + GLenum vaoCacheGlIndexType; // GL_UNSIGNED_INT or GL_UNSIGNED_SHORT + size_t vaoCacheGlIndexSize; // must be <= sizeof( vaoCacheGlIndex_t ) + // OpenGL ES extensions qboolean readDepth; qboolean readStencil; @@ -2220,6 +2225,7 @@ void R_VaoList_f(void); void RB_UpdateTessVao(unsigned int attribBits); void VaoCache_Commit(void); +void VaoCache_DrawElements(int numIndexes, int firstIndex); void VaoCache_Init(void); void VaoCache_BindVao(void); void VaoCache_CheckAdd(qboolean *endSurface, qboolean *recycleVertexBuffer, qboolean *recycleIndexBuffer, int numVerts, int numIndexes); diff --git a/code/renderergl2/tr_shade.c b/code/renderergl2/tr_shade.c index 21f9543a..dec35e52 100644 --- a/code/renderergl2/tr_shade.c +++ b/code/renderergl2/tr_shade.c @@ -40,7 +40,14 @@ R_DrawElements void R_DrawElements( int numIndexes, int firstIndex ) { - qglDrawElements(GL_TRIANGLES, numIndexes, GL_INDEX_TYPE, BUFFER_OFFSET(firstIndex * sizeof(glIndex_t))); + if (tess.useCacheVao) + { + VaoCache_DrawElements(numIndexes, firstIndex); + } + else + { + qglDrawElements(GL_TRIANGLES, numIndexes, GL_INDEX_TYPE, BUFFER_OFFSET(firstIndex * sizeof(glIndex_t))); + } } @@ -1720,6 +1727,8 @@ void RB_EndSurface( void ) { tess.numIndexes = 0; tess.numVertexes = 0; tess.firstIndex = 0; + tess.useCacheVao = qfalse; + tess.useInternalVao = qfalse; GLimp_LogComment( "----------\n" ); } diff --git a/code/renderergl2/tr_vbo.c b/code/renderergl2/tr_vbo.c index df64f8bc..5094df18 100644 --- a/code/renderergl2/tr_vbo.c +++ b/code/renderergl2/tr_vbo.c @@ -676,7 +676,7 @@ static struct srfVert_t vertexes[VAOCACHE_QUEUE_MAX_VERTEXES]; int vertexCommitSize; - glIndex_t indexes[VAOCACHE_QUEUE_MAX_INDEXES]; + vaoCacheGlIndex_t indexes[VAOCACHE_QUEUE_MAX_INDEXES]; int indexCommitSize; } vcq; @@ -687,18 +687,13 @@ vcq; // srfVert_t is 60 bytes // assuming each vert is referenced 4 times, need 16 bytes (4 glIndex_t) per vert // -> need about 4/15ths the space for indexes as vertexes -#if GL_INDEX_TYPE == GL_UNSIGNED_SHORT -#define VAOCACHE_VERTEX_BUFFER_SIZE (sizeof(srfVert_t) * USHRT_MAX) -#define VAOCACHE_INDEX_BUFFER_SIZE (sizeof(glIndex_t) * USHRT_MAX * 4) -#else // GL_UNSIGNED_INT #define VAOCACHE_VERTEX_BUFFER_SIZE (16 * 1024 * 1024) #define VAOCACHE_INDEX_BUFFER_SIZE (5 * 1024 * 1024) -#endif typedef struct buffered_s { - void *data; - int size; + glIndex_t *indexes; + int numIndexes; int bufferOffset; } buffered_t; @@ -736,7 +731,7 @@ void VaoCache_Commit(void) buffered_t *indexSet2 = indexSet; for (surf = vcq.surfaces; surf < end; surf++, indexSet2++) { - if (surf->indexes != indexSet2->data || (surf->numIndexes * sizeof(glIndex_t)) != indexSet2->size) + if (surf->indexes != indexSet2->indexes || surf->numIndexes != indexSet2->numIndexes) break; } @@ -750,7 +745,7 @@ void VaoCache_Commit(void) // If found, use it if (indexSet < vc.surfaceIndexSets + vc.numSurfaces) { - tess.firstIndex = indexSet->bufferOffset / sizeof(glIndex_t); + tess.firstIndex = indexSet->bufferOffset / glRefConfig.vaoCacheGlIndexSize; //ri.Printf(PRINT_ALL, "firstIndex %d numIndexes %d as %d\n", tess.firstIndex, tess.numIndexes, (int)(batchLength - vc.batchLengths)); //ri.Printf(PRINT_ALL, "vc.numSurfaces %d vc.numBatches %d\n", vc.numSurfaces, vc.numBatches); } @@ -759,20 +754,21 @@ void VaoCache_Commit(void) else { srfVert_t *dstVertex = vcq.vertexes; - glIndex_t *dstIndex = vcq.indexes; + vaoCacheGlIndex_t *dstIndex = vcq.indexes; + unsigned short *dstIndexUshort = (unsigned short *)vcq.indexes; batchLength = vc.batchLengths + vc.numBatches; *batchLength = vcq.numSurfaces; vc.numBatches++; - tess.firstIndex = vc.indexOffset / sizeof(glIndex_t); + tess.firstIndex = vc.indexOffset / glRefConfig.vaoCacheGlIndexSize; vcq.vertexCommitSize = 0; vcq.indexCommitSize = 0; for (surf = vcq.surfaces; surf < end; surf++) { glIndex_t *srcIndex = surf->indexes; int vertexesSize = surf->numVerts * sizeof(srfVert_t); - int indexesSize = surf->numIndexes * sizeof(glIndex_t); + int indexesSize = surf->numIndexes * glRefConfig.vaoCacheGlIndexSize; int i, indexOffset = (vc.vertexOffset + vcq.vertexCommitSize) / sizeof(srfVert_t); Com_Memcpy(dstVertex, surf->vertexes, vertexesSize); @@ -781,13 +777,21 @@ void VaoCache_Commit(void) vcq.vertexCommitSize += vertexesSize; indexSet = vc.surfaceIndexSets + vc.numSurfaces; - indexSet->data = surf->indexes; - indexSet->size = indexesSize; + indexSet->indexes = surf->indexes; + indexSet->numIndexes = surf->numIndexes; indexSet->bufferOffset = vc.indexOffset + vcq.indexCommitSize; vc.numSurfaces++; - for (i = 0; i < surf->numIndexes; i++) - *dstIndex++ = *srcIndex++ + indexOffset; + if (glRefConfig.vaoCacheGlIndexType == GL_UNSIGNED_SHORT) + { + for (i = 0; i < surf->numIndexes; i++) + *dstIndexUshort++ = *srcIndex++ + indexOffset; + } + else + { + for (i = 0; i < surf->numIndexes; i++) + *dstIndex++ = *srcIndex++ + indexOffset; + } vcq.indexCommitSize += indexesSize; } @@ -810,9 +814,30 @@ void VaoCache_Commit(void) } } +void VaoCache_DrawElements(int numIndexes, int firstIndex) +{ + assert( glState.currentVao == vc.vao ); + + qglDrawElements(GL_TRIANGLES, numIndexes, glRefConfig.vaoCacheGlIndexType, BUFFER_OFFSET(firstIndex * glRefConfig.vaoCacheGlIndexSize)); +} + void VaoCache_Init(void) { - vc.vao = R_CreateVao("VaoCache", NULL, VAOCACHE_VERTEX_BUFFER_SIZE, NULL, VAOCACHE_INDEX_BUFFER_SIZE, VAO_USAGE_DYNAMIC); + int vertexBufferSize; + int indexBufferSize; + + if (glRefConfig.vaoCacheGlIndexType == GL_UNSIGNED_SHORT) + { + vertexBufferSize = sizeof(srfVert_t) * USHRT_MAX; + indexBufferSize = sizeof(unsigned short) * USHRT_MAX * 4; + } + else + { + vertexBufferSize = VAOCACHE_VERTEX_BUFFER_SIZE; + indexBufferSize = VAOCACHE_INDEX_BUFFER_SIZE; + } + + vc.vao = R_CreateVao("VaoCache", NULL, vertexBufferSize, NULL, indexBufferSize, VAO_USAGE_DYNAMIC); vc.vao->attribs[ATTR_INDEX_POSITION].enabled = 1; vc.vao->attribs[ATTR_INDEX_TEXCOORD].enabled = 1; @@ -881,7 +906,7 @@ void VaoCache_BindVao(void) void VaoCache_CheckAdd(qboolean *endSurface, qboolean *recycleVertexBuffer, qboolean *recycleIndexBuffer, int numVerts, int numIndexes) { int vertexesSize = sizeof(srfVert_t) * numVerts; - int indexesSize = sizeof(glIndex_t) * numIndexes; + int indexesSize = glRefConfig.vaoCacheGlIndexSize * numIndexes; if (vc.vao->vertexesSize < vc.vertexOffset + vcq.vertexCommitSize + vertexesSize) { @@ -924,7 +949,7 @@ void VaoCache_CheckAdd(qboolean *endSurface, qboolean *recycleVertexBuffer, qboo *endSurface = qtrue; } - if (VAOCACHE_QUEUE_MAX_INDEXES * sizeof(glIndex_t) < vcq.indexCommitSize + indexesSize) + if (VAOCACHE_QUEUE_MAX_INDEXES * glRefConfig.vaoCacheGlIndexSize < vcq.indexCommitSize + indexesSize) { //ri.Printf(PRINT_ALL, "out of queued indexes\n"); *endSurface = qtrue; @@ -964,5 +989,5 @@ void VaoCache_AddSurface(srfVert_t *verts, int numVerts, glIndex_t *indexes, int vcq.numSurfaces++; vcq.vertexCommitSize += sizeof(srfVert_t) * numVerts; - vcq.indexCommitSize += sizeof(glIndex_t) * numIndexes; + vcq.indexCommitSize += glRefConfig.vaoCacheGlIndexSize * numIndexes; } From 98b9008c0d6138a2d00b8620a612ad7541aba3e4 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Fri, 20 Jul 2018 23:40:48 -0500 Subject: [PATCH 07/34] OpenGL2: Use CPU vertex animation if too few vertex attributes OpenGL ES has a minimum of 8 vertex attributes while desktop OpenGL has a minimum of 16. Vertex animation uses attributes 10 to 12. --- code/renderergl2/tr_glsl.c | 33 ++++++++++++++++++++++++++++----- code/renderergl2/tr_init.c | 3 +++ code/renderergl2/tr_local.h | 3 +++ code/renderergl2/tr_mesh.c | 19 +++++++++++++------ code/renderergl2/tr_model.c | 6 ++++++ 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/code/renderergl2/tr_glsl.c b/code/renderergl2/tr_glsl.c index bf93a2f4..39223bf6 100644 --- a/code/renderergl2/tr_glsl.c +++ b/code/renderergl2/tr_glsl.c @@ -968,6 +968,15 @@ void GLSL_InitGPUShaders(void) startTime = ri.Milliseconds(); + // OpenGL ES may not have enough attributes to fit ones used for vertex animation + if ( glRefConfig.maxVertexAttribs > ATTR_INDEX_NORMAL2 ) { + ri.Printf(PRINT_ALL, "Using GPU vertex animation\n"); + glRefConfig.gpuVertexAnimation = qtrue; + } else { + ri.Printf(PRINT_ALL, "Using CPU vertex animation\n"); + glRefConfig.gpuVertexAnimation = qfalse; + } + for (i = 0; i < GENERICDEF_COUNT; i++) { if ((i & GENERICDEF_USE_VERTEX_ANIMATION) && (i & GENERICDEF_USE_BONE_ANIMATION)) @@ -990,6 +999,9 @@ void GLSL_InitGPUShaders(void) if (i & GENERICDEF_USE_VERTEX_ANIMATION) { + if (!glRefConfig.gpuVertexAnimation) + continue; + Q_strcat(extradefines, 1024, "#define USE_VERTEX_ANIMATION\n"); attribs |= ATTR_POSITION2 | ATTR_NORMAL2; } @@ -1041,6 +1053,9 @@ void GLSL_InitGPUShaders(void) if ((i & FOGDEF_USE_VERTEX_ANIMATION) && (i & FOGDEF_USE_BONE_ANIMATION)) continue; + if ((i & FOGDEF_USE_VERTEX_ANIMATION) && !glRefConfig.gpuVertexAnimation) + continue; + if ((i & FOGDEF_USE_BONE_ANIMATION) && !glRefConfig.glslMaxAnimatedBones) continue; @@ -1221,12 +1236,17 @@ void GLSL_InitGPUShaders(void) if (i & LIGHTDEF_ENTITY_VERTEX_ANIMATION) { - Q_strcat(extradefines, 1024, "#define USE_VERTEX_ANIMATION\n#define USE_MODELMATRIX\n"); - attribs |= ATTR_POSITION2 | ATTR_NORMAL2; + Q_strcat(extradefines, 1024, "#define USE_MODELMATRIX\n"); - if (r_normalMapping->integer) + if (glRefConfig.gpuVertexAnimation) { - attribs |= ATTR_TANGENT2; + Q_strcat(extradefines, 1024, "#define USE_VERTEX_ANIMATION\n"); + attribs |= ATTR_POSITION2 | ATTR_NORMAL2; + + if (r_normalMapping->integer) + { + attribs |= ATTR_TANGENT2; + } } } else if (i & LIGHTDEF_ENTITY_BONE_ANIMATION) @@ -1261,6 +1281,9 @@ void GLSL_InitGPUShaders(void) if ((i & SHADOWMAPDEF_USE_VERTEX_ANIMATION) && (i & SHADOWMAPDEF_USE_BONE_ANIMATION)) continue; + if ((i & SHADOWMAPDEF_USE_VERTEX_ANIMATION) && !glRefConfig.gpuVertexAnimation) + continue; + if ((i & SHADOWMAPDEF_USE_BONE_ANIMATION) && !glRefConfig.glslMaxAnimatedBones) continue; @@ -1521,7 +1544,7 @@ void GLSL_ShutdownGPUShaders(void) ri.Printf(PRINT_ALL, "------- GLSL_ShutdownGPUShaders -------\n"); - for (i = 0; i < ATTR_INDEX_COUNT; i++) + for (i = 0; i < ATTR_INDEX_COUNT && i < glRefConfig.maxVertexAttribs; i++) qglDisableVertexAttribArray(i); GL_BindNullProgram(); diff --git a/code/renderergl2/tr_init.c b/code/renderergl2/tr_init.c index 12570392..aad662c5 100644 --- a/code/renderergl2/tr_init.c +++ b/code/renderergl2/tr_init.c @@ -279,6 +279,9 @@ static void InitOpenGL( void ) qglGetIntegerv( GL_MAX_TEXTURE_IMAGE_UNITS, &temp ); glConfig.numTextureUnits = temp; + qglGetIntegerv( GL_MAX_VERTEX_ATTRIBS, &temp ); + glRefConfig.maxVertexAttribs = temp; + // reserve 160 components for other uniforms qglGetIntegerv( GL_MAX_VERTEX_UNIFORM_COMPONENTS, &temp ); glRefConfig.glslMaxAnimatedBones = Com_Clamp( 0, IQM_MAX_JOINTS, ( temp - 160 ) / 16 ); diff --git a/code/renderergl2/tr_local.h b/code/renderergl2/tr_local.h index eab069e1..9f46866f 100644 --- a/code/renderergl2/tr_local.h +++ b/code/renderergl2/tr_local.h @@ -1433,6 +1433,9 @@ typedef struct { qboolean vertexArrayObject; qboolean directStateAccess; + int maxVertexAttribs; + qboolean gpuVertexAnimation; + GLenum vaoCacheGlIndexType; // GL_UNSIGNED_INT or GL_UNSIGNED_SHORT size_t vaoCacheGlIndexSize; // must be <= sizeof( vaoCacheGlIndex_t ) diff --git a/code/renderergl2/tr_mesh.c b/code/renderergl2/tr_mesh.c index 4f2c6724..222b06d0 100644 --- a/code/renderergl2/tr_mesh.c +++ b/code/renderergl2/tr_mesh.c @@ -282,9 +282,10 @@ R_AddMD3Surfaces */ void R_AddMD3Surfaces( trRefEntity_t *ent ) { int i; - mdvModel_t *model = NULL; - mdvSurface_t *surface = NULL; - shader_t *shader = NULL; + mdvModel_t *model; + mdvSurface_t *surface; + void *drawSurf; + shader_t *shader; int cull; int lod; int fogNum; @@ -382,6 +383,12 @@ void R_AddMD3Surfaces( trRefEntity_t *ent ) { shader = tr.shaders[ surface->shaderIndexes[ ent->e.skinNum % surface->numShaderIndexes ] ]; } + if ( model->numVaoSurfaces > 0 ) { + drawSurf = &model->vaoSurfaces[i]; + } else { + drawSurf = surface; + } + // we will add shadows even if the main object isn't visible in the view // stencil shadows can't do personal models unless I polyhedron clip @@ -390,7 +397,7 @@ void R_AddMD3Surfaces( trRefEntity_t *ent ) { && fogNum == 0 && !(ent->e.renderfx & ( RF_NOSHADOW | RF_DEPTHHACK ) ) && shader->sort == SS_OPAQUE ) { - R_AddDrawSurf( (void *)&model->vaoSurfaces[i], tr.shadowShader, 0, qfalse, qfalse, 0 ); + R_AddDrawSurf( drawSurf, tr.shadowShader, 0, qfalse, qfalse, 0 ); } // projection shadows work fine with personal models @@ -398,12 +405,12 @@ void R_AddMD3Surfaces( trRefEntity_t *ent ) { && fogNum == 0 && (ent->e.renderfx & RF_SHADOW_PLANE ) && shader->sort == SS_OPAQUE ) { - R_AddDrawSurf( (void *)&model->vaoSurfaces[i], tr.projectionShadowShader, 0, qfalse, qfalse, 0 ); + R_AddDrawSurf( drawSurf, tr.projectionShadowShader, 0, qfalse, qfalse, 0 ); } // don't add third_person objects if not viewing through a portal if ( !personalModel ) { - R_AddDrawSurf((void *)&model->vaoSurfaces[i], shader, fogNum, qfalse, qfalse, cubemapIndex ); + R_AddDrawSurf( drawSurf, shader, fogNum, qfalse, qfalse, cubemapIndex ); } surface++; diff --git a/code/renderergl2/tr_model.c b/code/renderergl2/tr_model.c index 0fdf60d8..7201be1b 100644 --- a/code/renderergl2/tr_model.c +++ b/code/renderergl2/tr_model.c @@ -664,6 +664,12 @@ static qboolean R_LoadMD3(model_t * mod, int lod, void *buffer, int bufferSize, surf++; } + if (mdvModel->numFrames > 1 && !glRefConfig.gpuVertexAnimation) + { + mdvModel->numVaoSurfaces = 0; + mdvModel->vaoSurfaces = NULL; + } + else { srfVaoMdvMesh_t *vaoSurf; From 551fa6c79778ada37639348e928f58300ac54ab0 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Thu, 6 Jun 2024 16:12:08 -0500 Subject: [PATCH 08/34] OpenGL2: Fix issues running under WebGL --- code/renderercommon/qgl.h | 2 +- code/renderergl2/glsl/depthblur_fp.glsl | 5 ++--- code/renderergl2/glsl/ssao_fp.glsl | 3 +-- code/renderergl2/tr_extensions.c | 17 ++++++++++++++--- code/renderergl2/tr_init.c | 7 ++++++- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/code/renderercommon/qgl.h b/code/renderercommon/qgl.h index 38f9919f..f226e408 100644 --- a/code/renderercommon/qgl.h +++ b/code/renderercommon/qgl.h @@ -80,7 +80,6 @@ extern void (APIENTRYP qglUnlockArraysEXT) (void); GLE(void, TexParameterf, GLenum target, GLenum pname, GLfloat param) \ GLE(void, TexParameteri, GLenum target, GLenum pname, GLint param) \ GLE(void, TexSubImage2D, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid *pixels) \ - GLE(void, Translatef, GLfloat x, GLfloat y, GLfloat z) \ GLE(void, Viewport, GLint x, GLint y, GLsizei width, GLsizei height) \ // OpenGL 1.0/1.1 and OpenGL ES 1.x but not OpenGL 3.2 core profile @@ -98,6 +97,7 @@ extern void (APIENTRYP qglUnlockArraysEXT) (void); GLE(void, ShadeModel, GLenum mode) \ GLE(void, TexCoordPointer, GLint size, GLenum type, GLsizei stride, const GLvoid *ptr) \ GLE(void, TexEnvf, GLenum target, GLenum pname, GLfloat param) \ + GLE(void, Translatef, GLfloat x, GLfloat y, GLfloat z) \ GLE(void, VertexPointer, GLint size, GLenum type, GLsizei stride, const GLvoid *ptr) \ // OpenGL 1.0/1.1 and 3.2 core profile but not OpenGL ES 1.x diff --git a/code/renderergl2/glsl/depthblur_fp.glsl b/code/renderergl2/glsl/depthblur_fp.glsl index d63df88a..205ee451 100644 --- a/code/renderergl2/glsl/depthblur_fp.glsl +++ b/code/renderergl2/glsl/depthblur_fp.glsl @@ -52,10 +52,9 @@ vec4 depthGaussian1D(sampler2D imageMap, sampler2D depthMap, vec2 tex, float zFa #endif float zLimit = 5.0 / zFar; - int i, j; - for (i = 0; i < 2; i++) + for (int i = 0; i < 2; i++) { - for (j = 1; j < BLUR_SIZE; j++) + for (int j = 1; j < BLUR_SIZE; j++) { vec2 offset = direction * (float(j) - 0.25) + nudge; #if defined(USE_DEPTH) diff --git a/code/renderergl2/glsl/ssao_fp.glsl b/code/renderergl2/glsl/ssao_fp.glsl index f3054404..2171f6e7 100644 --- a/code/renderergl2/glsl/ssao_fp.glsl +++ b/code/renderergl2/glsl/ssao_fp.glsl @@ -77,8 +77,7 @@ float ambientOcclusion(sampler2D depthMap, const vec2 tex, const float zFarDivZN float invZFar = 1.0 / zFar; float zLimit = 20.0 * invZFar; - int i; - for (i = 0; i < NUM_SAMPLES; i++) + for (int i = 0; i < NUM_SAMPLES; i++) { vec2 offset = rmat * poissonDisc[i] * offsetScale; float sampleDiff = getLinearDepth(depthMap, tex + offset, zFarDivZNear) - sampleZ; diff --git a/code/renderergl2/tr_extensions.c b/code/renderergl2/tr_extensions.c index 8603794b..856526bf 100644 --- a/code/renderergl2/tr_extensions.c +++ b/code/renderergl2/tr_extensions.c @@ -73,12 +73,23 @@ void GLimp_InitExtraExtensions(void) goto done; extension = "GL_EXT_occlusion_query_boolean"; - if (SDL_GL_ExtensionSupported(extension)) + if (qglesMajorVersion >= 3 || SDL_GL_ExtensionSupported(extension)) { glRefConfig.occlusionQuery = qtrue; glRefConfig.occlusionQueryTarget = GL_ANY_SAMPLES_PASSED; - QGL_ARB_occlusion_query_PROCS; + if (qglesMajorVersion >= 3) { + QGL_ARB_occlusion_query_PROCS; + } else { + // GL_EXT_occlusion_query_boolean uses EXT suffix +#undef GLE +#define GLE(ret, name, ...) qgl##name = (name##proc *) SDL_GL_GetProcAddress("gl" #name "EXT"); + + QGL_ARB_occlusion_query_PROCS; + +#undef GLE +#define GLE(ret, name, ...) qgl##name = (name##proc *) SDL_GL_GetProcAddress("gl" #name); + } ri.Printf(PRINT_ALL, result[glRefConfig.occlusionQuery], extension); } @@ -137,7 +148,7 @@ void GLimp_InitExtraExtensions(void) // GL_OES_element_index_uint extension = "GL_OES_element_index_uint"; - if (SDL_GL_ExtensionSupported(extension)) + if (qglesMajorVersion >= 3 || SDL_GL_ExtensionSupported(extension)) { glRefConfig.vaoCacheGlIndexType = GL_UNSIGNED_INT; glRefConfig.vaoCacheGlIndexSize = sizeof(unsigned int); diff --git a/code/renderergl2/tr_init.c b/code/renderergl2/tr_init.c index aad662c5..48da9ce3 100644 --- a/code/renderergl2/tr_init.c +++ b/code/renderergl2/tr_init.c @@ -283,7 +283,12 @@ static void InitOpenGL( void ) glRefConfig.maxVertexAttribs = temp; // reserve 160 components for other uniforms - qglGetIntegerv( GL_MAX_VERTEX_UNIFORM_COMPONENTS, &temp ); + if ( qglesMajorVersion ) { + qglGetIntegerv( GL_MAX_VERTEX_UNIFORM_VECTORS, &temp ); + temp *= 4; + } else { + qglGetIntegerv( GL_MAX_VERTEX_UNIFORM_COMPONENTS, &temp ); + } glRefConfig.glslMaxAnimatedBones = Com_Clamp( 0, IQM_MAX_JOINTS, ( temp - 160 ) / 16 ); if ( glRefConfig.glslMaxAnimatedBones < 12 ) { glRefConfig.glslMaxAnimatedBones = 0; From f869bffe971de35fa1c26156121f2fd8e143ff1e Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Thu, 6 Jun 2024 17:05:04 -0500 Subject: [PATCH 09/34] OpenGL2: Fallback to OpenGL ES if OpenGL fails --- README.md | 7 +- code/sdl/sdl_glimp.c | 192 +++++++++++++++++++++---------------------- opengl2-readme.md | 8 +- 3 files changed, 104 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index f2031c73..a5c53379 100644 --- a/README.md +++ b/README.md @@ -168,11 +168,12 @@ embedded System-on-a-Chip and mobile platforms. The opengl1 renderer does not have OpenGL ES support. -The `r_useOpenGLES` cvar controls whether to use OpenGL or OpenGL ES API. -Set to -1 for auto (default), 0 for OpenGL, and 1 for OpenGL ES. It should be +The opengl2 renderer will try both OpenGL and OpenGL ES APIs to find one that +works. The `r_preferOpenGLES` cvar controls which API to try first. +Set it to -1 for auto (default), 0 for OpenGL, and 1 for OpenGL ES. It should be set using command line arguments: - ioquake3 +set cl_renderer opengl2 +set r_useOpenGLES 1 + ioquake3 +set cl_renderer opengl2 +set r_preferOpenGLES 1 # Console diff --git a/code/sdl/sdl_glimp.c b/code/sdl/sdl_glimp.c index b24759b5..a3c5bb82 100644 --- a/code/sdl/sdl_glimp.c +++ b/code/sdl/sdl_glimp.c @@ -52,7 +52,7 @@ cvar_t *r_allowSoftwareGL; // Don't abort out if a hardware visual can't be obta cvar_t *r_allowResize; // make window resizable cvar_t *r_centerWindow; cvar_t *r_sdlDriver; -cvar_t *r_useOpenGLES; +cvar_t *r_preferOpenGLES; int qglMajorVersion, qglMinorVersion; int qglesMajorVersion, qglesMinorVersion; @@ -394,6 +394,12 @@ GLimp_SetMode */ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qboolean fixedFunction) { + struct GLimp_ContextType { + int profileMask; + int majorVersion; + int minorVersion; + } contexts[3]; + int numContexts, type; const char *glstring; int perChannelColorBits; int colorBits, depthBits, stencilBits; @@ -524,6 +530,48 @@ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qbool stencilBits = r_stencilbits->value; samples = r_ext_multisample->value; + numContexts = 0; + + if ( !fixedFunction ) { + int profileMask; + qboolean preferOpenGLES; + + SDL_GL_ResetAttributes(); + SDL_GL_GetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, &profileMask ); + + preferOpenGLES = ( r_preferOpenGLES->integer == 1 || + ( r_preferOpenGLES->integer == -1 && profileMask == SDL_GL_CONTEXT_PROFILE_ES ) ); + + if ( preferOpenGLES ) { + contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES; + contexts[numContexts].majorVersion = 2; + contexts[numContexts].minorVersion = 0; + numContexts++; + } + + contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_CORE; + contexts[numContexts].majorVersion = 3; + contexts[numContexts].minorVersion = 2; + numContexts++; + + contexts[numContexts].profileMask = 0; + contexts[numContexts].majorVersion = 2; + contexts[numContexts].minorVersion = 0; + numContexts++; + + if ( !preferOpenGLES ) { + contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES; + contexts[numContexts].majorVersion = 2; + contexts[numContexts].minorVersion = 0; + numContexts++; + } + } else { + contexts[numContexts].profileMask = 0; + contexts[numContexts].majorVersion = 1; + contexts[numContexts].minorVersion = 1; + numContexts++; + } + for (i = 0; i < 16; i++) { int testColorBits, testDepthBits, testStencilBits; @@ -656,116 +704,68 @@ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qbool SDL_SetWindowIcon( SDL_window, icon ); - if (!fixedFunction) - { - int profileMask; - - SDL_GL_GetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, &profileMask ); - - if ( r_useOpenGLES->integer == 1 || ( r_useOpenGLES->integer == -1 && profileMask == SDL_GL_CONTEXT_PROFILE_ES ) ) - { - ri.Printf( PRINT_ALL, "Trying to get an OpenGL ES 2.0 context\n" ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 2 ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 0 ); + for ( type = 0; type < numContexts; type++ ) { + char contextName[32]; - SDL_glContext = SDL_GL_CreateContext( SDL_window ); - if ( !SDL_glContext ) - { - ri.Printf( PRINT_ALL, "SDL_GL_CreateContext failed: %s\n", SDL_GetError() ); - } - else - { - ri.Printf( PRINT_ALL, "SDL_GL_CreateContext succeeded.\n" ); - - if ( !GLimp_GetProcAddresses( fixedFunction ) ) - { - ri.Printf( PRINT_ALL, "GLimp_GetProcAddresses() failed for OpenGL ES 2.0 context\n" ); - GLimp_ClearProcAddresses(); - SDL_GL_DeleteContext( SDL_glContext ); - SDL_glContext = NULL; - } - } + switch ( contexts[type].profileMask ) { + default: + case 0: + Com_sprintf( contextName, sizeof( contextName ), "OpenGL %d.%d", + contexts[type].majorVersion, contexts[type].minorVersion ); + break; + case SDL_GL_CONTEXT_PROFILE_CORE: + Com_sprintf( contextName, sizeof( contextName ), "OpenGL %d.%d Core", + contexts[type].majorVersion, contexts[type].minorVersion ); + break; + case SDL_GL_CONTEXT_PROFILE_ES: + Com_sprintf( contextName, sizeof( contextName ), "OpenGL ES %d.%d", + contexts[type].majorVersion, contexts[type].minorVersion ); + break; } - if ( !SDL_glContext ) - { - ri.Printf( PRINT_ALL, "Trying to get an OpenGL 3.2 core context\n" ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 3 ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 2 ); - - SDL_glContext = SDL_GL_CreateContext( SDL_window ); - if ( !SDL_glContext ) - { - ri.Printf( PRINT_ALL, "SDL_GL_CreateContext failed: %s\n", SDL_GetError() ); - } - else - { - const char *renderer; - - ri.Printf( PRINT_ALL, "SDL_GL_CreateContext succeeded.\n" ); - - if ( GLimp_GetProcAddresses( fixedFunction ) ) - { - renderer = (const char *)qglGetString( GL_RENDERER ); - } - else - { - ri.Printf( PRINT_ALL, "GLimp_GetProcAddresses() failed for OpenGL 3.2 core context\n" ); - renderer = NULL; - } - - if ( !renderer || strstr( renderer, "Software Renderer" ) || strstr( renderer, "Software Rasterizer" ) ) - { - if ( renderer ) - ri.Printf(PRINT_ALL, "GL_RENDERER is %s, rejecting context\n", renderer); - - GLimp_ClearProcAddresses(); - SDL_GL_DeleteContext( SDL_glContext ); - SDL_glContext = NULL; - } - } - } + SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, contexts[type].profileMask ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, contexts[type].majorVersion ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, contexts[type].minorVersion ); + SDL_glContext = SDL_GL_CreateContext( SDL_window ); if ( !SDL_glContext ) { - ri.Printf( PRINT_ALL, "Trying to get an OpenGL 2.0 context\n" ); - - SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, 0 ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 2 ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 0 ); - } - } - else - { - SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, 0 ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 1 ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 1 ); - - SDL_glContext = NULL; - } - - if ( !SDL_glContext ) - { - if( ( SDL_glContext = SDL_GL_CreateContext( SDL_window ) ) == NULL ) - { - ri.Printf( PRINT_DEVELOPER, "SDL_GL_CreateContext failed: %s\n", SDL_GetError( ) ); - SDL_DestroyWindow( SDL_window ); - SDL_window = NULL; + ri.Printf( PRINT_ALL, "SDL_GL_CreateContext() for %s context failed: %s\n", contextName, SDL_GetError() ); continue; } if ( !GLimp_GetProcAddresses( fixedFunction ) ) { - ri.Printf( PRINT_ALL, "GLimp_GetProcAddresses() failed\n" ); + ri.Printf( PRINT_ALL, "GLimp_GetProcAddresses() for %s context failed\n", contextName ); GLimp_ClearProcAddresses(); SDL_GL_DeleteContext( SDL_glContext ); SDL_glContext = NULL; - SDL_DestroyWindow( SDL_window ); - SDL_window = NULL; continue; } + + if ( contexts[type].profileMask == SDL_GL_CONTEXT_PROFILE_CORE ) { + const char *renderer; + + renderer = (const char *)qglGetString( GL_RENDERER ); + + if ( !renderer || strstr( renderer, "Software Renderer" ) || strstr( renderer, "Software Rasterizer" ) ) + { + ri.Printf( PRINT_ALL, "GL_RENDERER is %s, rejecting %s context\n", renderer, contextName ); + + GLimp_ClearProcAddresses(); + SDL_GL_DeleteContext( SDL_glContext ); + SDL_glContext = NULL; + continue; + } + } + + break; + } + + if ( !SDL_glContext ) { + SDL_DestroyWindow( SDL_window ); + SDL_window = NULL; + continue; } qglClearColor( 0, 0, 0, 1 ); @@ -1055,7 +1055,7 @@ void GLimp_Init( qboolean fixedFunction ) r_sdlDriver = ri.Cvar_Get( "r_sdlDriver", "", CVAR_ROM ); r_allowResize = ri.Cvar_Get( "r_allowResize", "0", CVAR_ARCHIVE | CVAR_LATCH ); r_centerWindow = ri.Cvar_Get( "r_centerWindow", "0", CVAR_ARCHIVE | CVAR_LATCH ); - r_useOpenGLES = ri.Cvar_Get( "r_useOpenGLES", "-1", CVAR_ARCHIVE | CVAR_LATCH ); + r_preferOpenGLES = ri.Cvar_Get( "r_preferOpenGLES", "-1", CVAR_ARCHIVE | CVAR_LATCH ); if( ri.Cvar_VariableIntegerValue( "com_abnormalExit" ) ) { diff --git a/opengl2-readme.md b/opengl2-readme.md index 2e076cfa..c798d89c 100644 --- a/opengl2-readme.md +++ b/opengl2-readme.md @@ -65,10 +65,10 @@ For Win32: Cvars for API: -* `r_useOpenGLES` - This enables using OpenGL ES 2+. - Many features are not supported such as sun shadows and HDR. - 1 - Use OpenGL ES. - 0 - Use desktop OpenGL. +* `r_preferOpenGLES` - This sets the preference for using OpenGL or OpenGL ES 2. + Many features are not supported when using OpenGL ES such as sun shadows and HDR. + 1 - Prefer OpenGL ES 2+. + 0 - Prefer desktop OpenGL. -1 - Automatically pick (default). Cvars for simple rendering features: From f41bd37fde1ef7aee59522ad6a5124a0eb6b00ba Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Thu, 6 Jun 2024 18:26:07 -0500 Subject: [PATCH 10/34] Add minimal emscripten support --- Makefile | 62 +++++++++++++++++++++++++++++++++++++-- code/qcommon/q_platform.h | 16 ++++++++++ code/sdl/sdl_glimp.c | 17 ++++++++++- code/sys/sys_main.c | 8 +++++ 4 files changed, 100 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index f90c785c..57e471c5 100644 --- a/Makefile +++ b/Makefile @@ -1042,6 +1042,38 @@ ifeq ($(PLATFORM),sunos) else # ifeq sunos +############################################################################# +# SETUP AND BUILD -- emscripten +############################################################################# + +ifeq ($(PLATFORM),emscripten) + + # 1. Create "baseq3" directory in the same directory as this Makefile. + # 2. Copy pak[0-8].pk3 into the created "baseq3" directory. + # 3. Run `/path/to/emsdk.sh` + # 4. Run `make PLATFORM=emscripten` + # 5. Serve the build/release-emscripten-wasm32/ioquake3_opengl2.{html,js,wasm,data} from a web server. + # 6. Load ioquake3_opengl2.html in a web browser. + + CC=emcc + ARCH=wasm32 + + # LDFLAGS+=-s MAIN_MODULE is needed for dlopen() in client/server but it causes compile errors + USE_RENDERER_DLOPEN=0 + + BASE_CFLAGS=-fPIC -s USE_SDL=2 + LDFLAGS=-s TOTAL_MEMORY=256mb -s MAX_WEBGL_VERSION=2 --preload-file baseq3 + OPTIMIZEVM = -O3 + OPTIMIZE = $(OPTIMIZEVM) + + FULLBINEXT=.html + + SHLIBEXT=wasm + SHLIBCFLAGS=-fPIC + SHLIBLDFLAGS=-s SIDE_MODULE + +else # ifeq emscripten + ############################################################################# # SETUP AND BUILD -- GENERIC ############################################################################# @@ -1060,6 +1092,7 @@ endif #OpenBSD endif #NetBSD endif #IRIX endif #SunOS +endif #emscripten ifndef CC CC=gcc @@ -1085,18 +1118,37 @@ endif ifneq ($(BUILD_SERVER),0) TARGETS += $(B)/$(SERVERBIN)$(FULLBINEXT) + + ifeq ($(PLATFORM),emscripten) + EMSCRIPTENOBJ += $(B)/$(SERVERBIN).js \ + $(B)/$(SERVERBIN).wasm \ + $(B)/$(SERVERBIN).data + endif endif ifneq ($(BUILD_CLIENT),0) + TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT) + + ifeq ($(PLATFORM),emscripten) + EMSCRIPTENOBJ += $(B)/$(CLIENTBIN).js \ + $(B)/$(CLIENTBIN).wasm \ + $(B)/$(CLIENTBIN).data + endif + ifneq ($(USE_RENDERER_DLOPEN),0) - TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT) $(B)/renderer_opengl1_$(SHLIBNAME) + TARGETS += $(B)/renderer_opengl1_$(SHLIBNAME) ifneq ($(BUILD_RENDERER_OPENGL2),0) TARGETS += $(B)/renderer_opengl2_$(SHLIBNAME) endif else - TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT) ifneq ($(BUILD_RENDERER_OPENGL2),0) TARGETS += $(B)/$(CLIENTBIN)_opengl2$(FULLBINEXT) + + ifeq ($(PLATFORM),emscripten) + EMSCRIPTENOBJ += $(B)/$(CLIENTBIN)_opengl2.js \ + $(B)/$(CLIENTBIN)_opengl2.wasm \ + $(B)/$(CLIENTBIN)_opengl2.data + endif endif endif endif @@ -1870,10 +1922,15 @@ Q3OBJ = \ ifdef MINGW Q3OBJ += \ $(B)/client/con_passive.o +else +ifeq ($(PLATFORM),emscripten) + Q3OBJ += \ + $(B)/client/con_passive.o else Q3OBJ += \ $(B)/client/con_tty.o endif +endif Q3R2OBJ = \ $(B)/renderergl2/tr_animation.o \ @@ -3044,6 +3101,7 @@ clean2: @rm -f $(OBJ) @rm -f $(OBJ_D_FILES) @rm -f $(STRINGOBJ) + @rm -f $(EMSCRIPTENOBJ) @rm -f $(TARGETS) toolsclean: toolsclean-debug toolsclean-release diff --git a/code/qcommon/q_platform.h b/code/qcommon/q_platform.h index 72dbfe1d..53a53264 100644 --- a/code/qcommon/q_platform.h +++ b/code/qcommon/q_platform.h @@ -290,6 +290,22 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #endif +//================================================================== EMSCRIPTEN === + +#ifdef __EMSCRIPTEN__ + +#define OS_STRING "emscripten" +#define ID_INLINE inline +#define PATH_SEP '/' + +#define ARCH_STRING "wasm32" + +#define Q3_LITTLE_ENDIAN + +#define DLL_EXT ".wasm" + +#endif + //================================================================== Q3VM === #ifdef Q3_VM diff --git a/code/sdl/sdl_glimp.c b/code/sdl/sdl_glimp.c index a3c5bb82..2678dc71 100644 --- a/code/sdl/sdl_glimp.c +++ b/code/sdl/sdl_glimp.c @@ -398,7 +398,7 @@ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qbool int profileMask; int majorVersion; int minorVersion; - } contexts[3]; + } contexts[4]; int numContexts, type; const char *glstring; int perChannelColorBits; @@ -543,6 +543,14 @@ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qbool ( r_preferOpenGLES->integer == -1 && profileMask == SDL_GL_CONTEXT_PROFILE_ES ) ); if ( preferOpenGLES ) { +#ifdef __EMSCRIPTEN__ + // WebGL 2.0 isn't fully backward compatible so you have to ask for it specifically + contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES; + contexts[numContexts].majorVersion = 3; + contexts[numContexts].minorVersion = 0; + numContexts++; +#endif + contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES; contexts[numContexts].majorVersion = 2; contexts[numContexts].minorVersion = 0; @@ -560,6 +568,13 @@ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qbool numContexts++; if ( !preferOpenGLES ) { +#ifdef __EMSCRIPTEN__ + contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES; + contexts[numContexts].majorVersion = 3; + contexts[numContexts].minorVersion = 0; + numContexts++; +#endif + contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES; contexts[numContexts].majorVersion = 2; contexts[numContexts].minorVersion = 0; diff --git a/code/sys/sys_main.c b/code/sys/sys_main.c index 0113920b..d7341806 100644 --- a/code/sys/sys_main.c +++ b/code/sys/sys_main.c @@ -31,6 +31,10 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include #include +#ifdef __EMSCRIPTEN__ +#include +#endif + #ifndef DEDICATED #ifdef USE_LOCAL_HEADERS # include "SDL.h" @@ -863,10 +867,14 @@ int main( int argc, char **argv ) signal( SIGTERM, Sys_SigHandler ); signal( SIGINT, Sys_SigHandler ); +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop( Com_Frame, 0, 1 ); +#else while( 1 ) { Com_Frame( ); } +#endif return 0; } From 4fc059b9402d2f813bd1251faf9c8228c99dab03 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Fri, 7 Jun 2024 18:00:27 -0500 Subject: [PATCH 11/34] Fix emscripten build directions --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 57e471c5..3641d561 100644 --- a/Makefile +++ b/Makefile @@ -1050,7 +1050,7 @@ ifeq ($(PLATFORM),emscripten) # 1. Create "baseq3" directory in the same directory as this Makefile. # 2. Copy pak[0-8].pk3 into the created "baseq3" directory. - # 3. Run `/path/to/emsdk.sh` + # 3. Run `source "/path/to/emsdk_env.sh"` to add emcc to PATH. # 4. Run `make PLATFORM=emscripten` # 5. Serve the build/release-emscripten-wasm32/ioquake3_opengl2.{html,js,wasm,data} from a web server. # 6. Load ioquake3_opengl2.html in a web browser. From 6f5c0389477c6c65c04be12ea2eb7a33a79e659f Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sat, 8 Jun 2024 22:16:14 -0500 Subject: [PATCH 12/34] Use BASEGAME for emscripten assets --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3641d561..21d5370d 100644 --- a/Makefile +++ b/Makefile @@ -1062,7 +1062,7 @@ ifeq ($(PLATFORM),emscripten) USE_RENDERER_DLOPEN=0 BASE_CFLAGS=-fPIC -s USE_SDL=2 - LDFLAGS=-s TOTAL_MEMORY=256mb -s MAX_WEBGL_VERSION=2 --preload-file baseq3 + LDFLAGS=-s TOTAL_MEMORY=256mb -s MAX_WEBGL_VERSION=2 --preload-file $(BASEGAME) OPTIMIZEVM = -O3 OPTIMIZE = $(OPTIMIZEVM) From 17cadb5369124c1c5f31a931fb248b7365e477d2 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 9 Jun 2024 01:21:58 -0500 Subject: [PATCH 13/34] Fix using emscripten 3.1.27+ emscripten 3.1.27 reduced the stack size from 5MB to 64KB. This caused run-time errors: Uncaught RuntimeError: index out of bounds Co-authored-by: James Darpinian --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 21d5370d..530c97d5 100644 --- a/Makefile +++ b/Makefile @@ -1062,7 +1062,7 @@ ifeq ($(PLATFORM),emscripten) USE_RENDERER_DLOPEN=0 BASE_CFLAGS=-fPIC -s USE_SDL=2 - LDFLAGS=-s TOTAL_MEMORY=256mb -s MAX_WEBGL_VERSION=2 --preload-file $(BASEGAME) + LDFLAGS=-s STACK_SIZE=5MB -s TOTAL_MEMORY=256MB -s MAX_WEBGL_VERSION=2 --preload-file $(BASEGAME) OPTIMIZEVM = -O3 OPTIMIZE = $(OPTIMIZEVM) From 25a680ab6ec16a2238ed157685a2b758b5a73649 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 9 Jun 2024 01:32:34 -0500 Subject: [PATCH 14/34] Allow building for emscripten with "emmake make" Co-authored-by: James Darpinian --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile b/Makefile index 530c97d5..9eb9462c 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,11 @@ ifeq ($(COMPILE_PLATFORM),cygwin) PLATFORM=mingw32 endif +# detect "emmake make" +ifeq ($(findstring /emcc,$(CC)),/emcc) + PLATFORM=emscripten +endif + ifndef PLATFORM PLATFORM=$(COMPILE_PLATFORM) endif From 413be19a8d21c67e35743084cb2349e128849ed3 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 9 Jun 2024 01:43:02 -0500 Subject: [PATCH 15/34] Add -ffast-math for emscripten to match other platforms Co-authored-by: James Darpinian --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9eb9462c..0bc9522b 100644 --- a/Makefile +++ b/Makefile @@ -1069,7 +1069,7 @@ ifeq ($(PLATFORM),emscripten) BASE_CFLAGS=-fPIC -s USE_SDL=2 LDFLAGS=-s STACK_SIZE=5MB -s TOTAL_MEMORY=256MB -s MAX_WEBGL_VERSION=2 --preload-file $(BASEGAME) OPTIMIZEVM = -O3 - OPTIMIZE = $(OPTIMIZEVM) + OPTIMIZE = $(OPTIMIZEVM) -ffast-math FULLBINEXT=.html From 37d78b6279401f4eded6adba628efc67e0961f6e Mon Sep 17 00:00:00 2001 From: James Darpinian Date: Fri, 7 Jun 2024 08:27:37 -0700 Subject: [PATCH 16/34] Add .vscode and baseq3 to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index eeb21870..f5d219f0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ Makefile.local *.swp *tags *~ +/.vscode/ +/baseq3 # OS X #################### From e247b316a72fb5b417455b6d78421081d6773993 Mon Sep 17 00:00:00 2001 From: James Darpinian Date: Fri, 7 Jun 2024 08:27:44 -0700 Subject: [PATCH 17/34] Rebuild every target if Makefile changes --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 0bc9522b..ae7549fa 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,10 @@ # # GNU Make required # + +# Rebuild every target if Makefile changes +.EXTRA_PREREQS:= $(abspath $(lastword $(MAKEFILE_LIST))) + COMPILE_PLATFORM=$(shell uname | sed -e 's/_.*//' | tr '[:upper:]' '[:lower:]' | sed -e 's/\//_/g') COMPILE_ARCH=$(shell uname -m | sed -e 's/i.86/x86/' | sed -e 's/^arm.*/arm/') From db24dfe13fccd1432a0712d0a38b494fcdc6b6af Mon Sep 17 00:00:00 2001 From: James Darpinian Date: Fri, 7 Jun 2024 08:27:56 -0700 Subject: [PATCH 18/34] ioquake3.html replaces Emscripten-generated HTML shell This enables several things: * Optionally load pk3 files from a web server at runtime instead of bundling them with Emscripten at build time * Set command line arguments via URL param * It's not ugly --- Makefile | 83 +++++++++++++++++++++++++------- code/web/baseq3/ioq3-config.json | 13 +++++ code/web/empty/ioq3-config.json | 3 ++ code/web/ioquake3.html | 70 +++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 18 deletions(-) create mode 100644 code/web/baseq3/ioq3-config.json create mode 100644 code/web/empty/ioq3-config.json create mode 100644 code/web/ioquake3.html diff --git a/Makefile b/Makefile index ae7549fa..eeee0f3a 100644 --- a/Makefile +++ b/Makefile @@ -1066,17 +1066,52 @@ ifeq ($(PLATFORM),emscripten) CC=emcc ARCH=wasm32 - + BINEXT=.js + # LDFLAGS+=-s MAIN_MODULE is needed for dlopen() in client/server but it causes compile errors USE_RENDERER_DLOPEN=0 + USE_OPENAL_DLOPEN=0 + USE_CURL=0 + HAVE_VM_COMPILED=false + BUILD_GAME_SO=0 + BUILD_GAME_QVM=0 + # Would be interesting to try to get the server working via WebRTC DataChannel. + # This would enable P2P play, hosting a server in the browser. Also, + # DataChannel is the only way to use UDP in the browser. + BUILD_SERVER=0 + + CLIENT_EXTRA_FILES+=code/web/ioquake3.html + + CLIENT_CFLAGS+=-s USE_SDL=2 + + CLIENT_LDFLAGS+=-s TOTAL_MEMORY=256mb + CLIENT_LDFLAGS+=-s STACK_SIZE=5MB + # Informing Emscripten which WebGL versions we support makes the JS bundle smaller and faster to load. + CLIENT_LDFLAGS+=-s MIN_WEBGL_VERSION=2 + CLIENT_LDFLAGS+=-s MAX_WEBGL_VERSION=2 + CLIENT_LDFLAGS+=-s FULL_ES2=1 + # The HTML file can use these functions to load extra files before the game starts. + CLIENT_LDFLAGS+=-s EXPORTED_RUNTIME_METHODS=FS,addRunDependency,removeRunDependency + CLIENT_LDFLAGS+=-s EXIT_RUNTIME=1 + CLIENT_LDFLAGS+=-s EXPORT_ES6 + CLIENT_LDFLAGS+=-s EXPORT_NAME=ioquake3 + # Game data files can be packaged by emcc into a .data file that lives next to the wasm bundle + # and added to the virtual filesystem before the game starts. This requires the game data to be + # present at build time and it can't be changed afterward. + # For more flexibility, game data files can be loaded from a web server at runtime by listing + # them in ioq3-config.json. This way they don't have to be present at build time and can be + # changed later. + ifneq ($(wildcard $(BASEGAME)/*),) + CLIENT_LDFLAGS+=--preload-file $(BASEGAME) + EMSCRIPTEN_PRELOAD_FILE=1 + CLIENT_EXTRA_FILES+=code/web/empty/ioq3-config.json + else + CLIENT_EXTRA_FILES+=code/web/$(BASEGAME)/ioq3-config.json + endif - BASE_CFLAGS=-fPIC -s USE_SDL=2 - LDFLAGS=-s STACK_SIZE=5MB -s TOTAL_MEMORY=256MB -s MAX_WEBGL_VERSION=2 --preload-file $(BASEGAME) OPTIMIZEVM = -O3 OPTIMIZE = $(OPTIMIZEVM) -ffast-math - FULLBINEXT=.html - SHLIBEXT=wasm SHLIBCFLAGS=-fPIC SHLIBLDFLAGS=-s SIDE_MODULE @@ -1129,19 +1164,16 @@ ifneq ($(BUILD_SERVER),0) TARGETS += $(B)/$(SERVERBIN)$(FULLBINEXT) ifeq ($(PLATFORM),emscripten) - EMSCRIPTENOBJ += $(B)/$(SERVERBIN).js \ - $(B)/$(SERVERBIN).wasm \ - $(B)/$(SERVERBIN).data + EMSCRIPTENOBJ+=$(B)/$(SERVERBIN).wasm + ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) + EMSCRIPTENOBJ+=$(B)/$(SERVERBIN).data + endif endif endif ifneq ($(BUILD_CLIENT),0) - TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT) - - ifeq ($(PLATFORM),emscripten) - EMSCRIPTENOBJ += $(B)/$(CLIENTBIN).js \ - $(B)/$(CLIENTBIN).wasm \ - $(B)/$(CLIENTBIN).data + ifneq ($(PLATFORM),emscripten) + TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT) endif ifneq ($(USE_RENDERER_DLOPEN),0) @@ -1154,9 +1186,10 @@ ifneq ($(BUILD_CLIENT),0) TARGETS += $(B)/$(CLIENTBIN)_opengl2$(FULLBINEXT) ifeq ($(PLATFORM),emscripten) - EMSCRIPTENOBJ += $(B)/$(CLIENTBIN)_opengl2.js \ - $(B)/$(CLIENTBIN)_opengl2.wasm \ - $(B)/$(CLIENTBIN)_opengl2.data + EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN)_opengl2.wasm32.wasm + ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) + EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN)_opengl2.wasm32.data + endif endif endif endif @@ -1504,6 +1537,7 @@ ifneq ($(BUILD_CLIENT),0) endif NAKED_TARGETS=$(shell echo $(TARGETS) | sed -e "s!$(B)/!!g") +NAKED_EMSCRIPTENOBJ=$(shell echo $(EMSCRIPTENOBJ) | sed -e "s!$(B)/!!g") print_list=-@for i in $(1); \ do \ @@ -1559,7 +1593,20 @@ endif @echo "" @echo " Output:" $(call print_list, $(NAKED_TARGETS)) + $(call print_list, $(NAKED_EMSCRIPTENOBJ)) + @echo "" +ifeq ($(PLATFORM),emscripten) +ifneq ($(EMSCRIPTEN_PRELOAD_FILE),1) + @echo " Warning: Game files not found in '$(BASEGAME)'." + @echo " They will not be packaged by Emscripten or preloaded." + @echo " To run this build you must serve the game files from a web server" + @echo " and list their paths in ioq3-config.json." + @echo " To make a build that automatically loads the game files, create a" + @echo " directory called '$(BASEGAME)' and copy your game files into it, then" + @echo " 'emmake make clean' and rebuild." @echo "" +endif +endif ifneq ($(TARGETS),) ifndef DEBUG_MAKEFILE @$(MAKE) $(TARGETS) $(B).zip V=$(V) @@ -1575,7 +1622,7 @@ endif ifneq ($(PLATFORM),darwin) ifdef ARCHIVE @rm -f $@ - @(cd $(B) && zip -r9 ../../$@ $(NAKED_TARGETS)) + @(cd $(B) && zip -r9 ../../$@ $(NAKED_TARGETS) $(NAKED_EMSCRIPTENOBJ)) endif endif diff --git a/code/web/baseq3/ioq3-config.json b/code/web/baseq3/ioq3-config.json new file mode 100644 index 00000000..b0cb1f16 --- /dev/null +++ b/code/web/baseq3/ioq3-config.json @@ -0,0 +1,13 @@ +{ + "files": [ + {"src": "baseq3/pak0.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak1.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak2.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak3.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak4.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak5.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak6.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak7.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak8.pk3", "dst": "/baseq3"} + ] +} diff --git a/code/web/empty/ioq3-config.json b/code/web/empty/ioq3-config.json new file mode 100644 index 00000000..158547c2 --- /dev/null +++ b/code/web/empty/ioq3-config.json @@ -0,0 +1,3 @@ +{ + "files": [] +} diff --git a/code/web/ioquake3.html b/code/web/ioquake3.html new file mode 100644 index 00000000..4aac449d --- /dev/null +++ b/code/web/ioquake3.html @@ -0,0 +1,70 @@ + +ioquake3 Emscripten demo + + + + + From 2765cd53980b054c97b8e25b188b4c6ce8f77f96 Mon Sep 17 00:00:00 2001 From: James Darpinian Date: Fri, 7 Jun 2024 08:28:01 -0700 Subject: [PATCH 19/34] Get rid of "Nothing to be done for _.zip" message --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index eeee0f3a..1531901e 100644 --- a/Makefile +++ b/Makefile @@ -1625,6 +1625,7 @@ ifneq ($(PLATFORM),darwin) @(cd $(B) && zip -r9 ../../$@ $(NAKED_TARGETS) $(NAKED_EMSCRIPTENOBJ)) endif endif + @: makedirs: @$(MKDIR) $(B)/autoupdater From 6fe061c36e6ed8df5f43ea01bfef006ab5cfafe7 Mon Sep 17 00:00:00 2001 From: James Darpinian Date: Fri, 7 Jun 2024 08:28:04 -0700 Subject: [PATCH 20/34] Fix build when specifying PLATFORM=emscripten manually instead of using emmake --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1531901e..a4501ef8 100644 --- a/Makefile +++ b/Makefile @@ -1064,7 +1064,9 @@ ifeq ($(PLATFORM),emscripten) # 5. Serve the build/release-emscripten-wasm32/ioquake3_opengl2.{html,js,wasm,data} from a web server. # 6. Load ioquake3_opengl2.html in a web browser. - CC=emcc + ifneq ($(findstring /emcc,$(CC)),/emcc) + CC=emcc + endif ARCH=wasm32 BINEXT=.js From 9348dbf64363d815cf09573cb928302d725836a8 Mon Sep 17 00:00:00 2001 From: James Darpinian Date: Fri, 7 Jun 2024 08:28:07 -0700 Subject: [PATCH 21/34] Silence compiler warnings --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index a4501ef8..9d73264f 100644 --- a/Makefile +++ b/Makefile @@ -1114,6 +1114,10 @@ ifeq ($(PLATFORM),emscripten) OPTIMIZEVM = -O3 OPTIMIZE = $(OPTIMIZEVM) -ffast-math + # These allow a warning-free build. + # Some of these warnings may actually be legit problems and should be fixed at some point. + BASE_CFLAGS+=-Wno-deprecated-non-prototype -Wno-dangling-else -Wno-implicit-const-int-float-conversion -Wno-misleading-indentation -Wno-format-overflow -Wno-logical-not-parentheses -Wno-absolute-value + SHLIBEXT=wasm SHLIBCFLAGS=-fPIC SHLIBLDFLAGS=-s SIDE_MODULE From f2169ed0b4b7dc60ab3b825b31353cdeea45bd21 Mon Sep 17 00:00:00 2001 From: James Darpinian Date: Fri, 7 Jun 2024 08:28:09 -0700 Subject: [PATCH 22/34] Support debug Emscripten build --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9d73264f..f6e4a11e 100644 --- a/Makefile +++ b/Makefile @@ -1118,6 +1118,10 @@ ifeq ($(PLATFORM),emscripten) # Some of these warnings may actually be legit problems and should be fixed at some point. BASE_CFLAGS+=-Wno-deprecated-non-prototype -Wno-dangling-else -Wno-implicit-const-int-float-conversion -Wno-misleading-indentation -Wno-format-overflow -Wno-logical-not-parentheses -Wno-absolute-value + DEBUG_CFLAGS=-g3 -O0 # -fsanitize=address -fsanitize=undefined + # Emscripten needs debug compiler flags to be passed to the linker as well + DEBUG_LDFLAGS=$(DEBUG_CFLAGS) + SHLIBEXT=wasm SHLIBCFLAGS=-fPIC SHLIBLDFLAGS=-s SIDE_MODULE @@ -1506,7 +1510,8 @@ all: debug release debug: @$(MAKE) targets B=$(BD) CFLAGS="$(CFLAGS) $(BASE_CFLAGS) $(DEPEND_CFLAGS)" \ OPTIMIZE="$(DEBUG_CFLAGS)" OPTIMIZEVM="$(DEBUG_CFLAGS)" \ - CLIENT_CFLAGS="$(CLIENT_CFLAGS)" SERVER_CFLAGS="$(SERVER_CFLAGS)" V=$(V) + CLIENT_CFLAGS="$(CLIENT_CFLAGS)" SERVER_CFLAGS="$(SERVER_CFLAGS)" V=$(V) \ + LDFLAGS="$(LDFLAGS) $(DEBUG_LDFLAGS)" release: @$(MAKE) targets B=$(BR) CFLAGS="$(CFLAGS) $(BASE_CFLAGS) $(DEPEND_CFLAGS)" \ From 1f9cbddf37c16fd1d9b05cded6fde90065dcddb9 Mon Sep 17 00:00:00 2001 From: James Darpinian Date: Fri, 7 Jun 2024 08:28:11 -0700 Subject: [PATCH 23/34] Speed up GitHub Actions with -j --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e13814a..8166f163 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: sudo apt-get update sudo apt-get install libsdl2-dev - name: Compile - run: make release + run: make release -j$(nproc) env: ARCHIVE: 1 - uses: actions/upload-artifact@v4 @@ -27,7 +27,7 @@ jobs: - name: Compile run: | choco install zip - make release + make release -j $env:NUMBER_OF_PROCESSORS env: ARCHIVE: 1 - uses: actions/upload-artifact@v4 @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Compile - run: make release + run: make release -j$(sysctl -n hw.logicalcpu) env: ARCHIVE: 1 - uses: actions/upload-artifact@v4 From 14a4278cdb2d556ab994668b99b28e51910f7a96 Mon Sep 17 00:00:00 2001 From: James Darpinian Date: Fri, 7 Jun 2024 08:28:14 -0700 Subject: [PATCH 24/34] Add GitHub Actions workflow for web/Emscripten --- .github/workflows/build.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8166f163..c20d6fd8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,3 +47,26 @@ jobs: with: name: macOS path: build/*.zip + web: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: emscripten-core/emsdk + path: emsdk + - name: Install Dependencies + run: | + cd emsdk + ./emsdk install 3.1.58 + ./emsdk activate 3.1.58 + - name: Compile + env: + ARCHIVE: 1 + run: | + source emsdk/emsdk_env.sh + emmake make release -j$(nproc) + - uses: actions/upload-artifact@v4 + with: + name: Web + path: build/*.zip From e0ce0ceea05396101d7a6c08f73cd1bcb8fe3aa5 Mon Sep 17 00:00:00 2001 From: James Darpinian Date: Fri, 7 Jun 2024 08:28:20 -0700 Subject: [PATCH 25/34] Add Emscripten to README --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index a5c53379..746cfa95 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Some of the major features currently implemented are: * Multiuser support on Windows systems (user specific game data is stored in "%APPDATA%\Quake3") * PNG support + * Web support via Emscripten * Many, many bug fixes The map editor and associated compiling tools are not included. We suggest you @@ -98,6 +99,23 @@ For macOS, building a Universal Binary 2 (macOS 10.9+, arm64, x86_64) 4. Copy the resulting ioquake3.app in /build/release-darwin-universal2 to your /Applications/ioquake3 folder. +For Web, building with Emscripten + 1. Follow the installation instructions for the Emscripten SDK including + setting up the environment with emsdk_env. + 2. Copy or symlink your baseq3 directory into this directory so Emscripten + can package your game files. (Alternatively, you can build without this, + and provide the game data files at runtime. For this, game files should + be listed in `ioq3-config.json`) + 3. Run `emmake make debug` (or release, but if you do both then you will + need to pass an extra URL parameter to the HTML file to select the + build you want). + 4. Start a web server serving this directory. `python3 -m http.server` + is an easy default that you may already have installed. + 5. Open `code/web/ioquake3.html` in a web browser. Open the developer + console to see errors and warnings. + 6. Debugging the C code is possible using a Chrome extension. For details + see https://developer.chrome.com/blog/wasm-debugging-2020 + Installation, for *nix 1. Set the COPYDIR variable in the shell to be where you installed Quake 3 to. By default it will be /usr/local/games/quake3 if you haven't set it. From 198c92b9f84fffede6558ca57dbb27eb0e8f4198 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 9 Jun 2024 21:49:31 -0500 Subject: [PATCH 26/34] Add DEPEND_MAKEFILE to disable rebuild on Makefile edit --- Makefile | 9 +++++---- README.md | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index f6e4a11e..ba45103e 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,6 @@ # # GNU Make required # - -# Rebuild every target if Makefile changes -.EXTRA_PREREQS:= $(abspath $(lastword $(MAKEFILE_LIST))) - COMPILE_PLATFORM=$(shell uname | sed -e 's/_.*//' | tr '[:upper:]' '[:lower:]' | sed -e 's/\//_/g') COMPILE_ARCH=$(shell uname -m | sed -e 's/i.86/x86/' | sed -e 's/^arm.*/arm/') @@ -3210,6 +3206,11 @@ dist: # DEPENDENCIES ############################################################################# +# Rebuild every target if Makefile or Makefile.local changes +ifneq ($(DEPEND_MAKEFILE),0) +.EXTRA_PREREQS:= $(MAKEFILE_LIST) +endif + ifneq ($(B),) OBJ_D_FILES=$(filter %.d,$(OBJ:%.o=%.d)) TOOLSOBJ_D_FILES=$(filter %.d,$(TOOLSOBJ:%.o=%.d)) diff --git a/README.md b/README.md index 746cfa95..51b27a8b 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,8 @@ The following variables may be set, either on the command line or in Makefile.local: ``` + DEPEND_MAKEFILE - set to 0 to disable rebuilding all targets when + the Makefile or Makefile.local is changed CFLAGS - use this for custom CFLAGS V - set to show cc command line when building DEFAULT_BASEDIR - extra path to search for baseq3 and such From b14c5a3d9c9f48a0d0fe0ed2bb28db54a9b4a43a Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 9 Jun 2024 21:56:03 -0500 Subject: [PATCH 27/34] Add BUILD_RENDERER_OPENGL1 to disable opengl1 Have emscripten disable BUILD_RENDERER_OPENGL1 instead of hardcoded to disable opengl1 client. --- Makefile | 29 ++++++++++++++++++++++++----- README.md | 2 ++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index ba45103e..8704d982 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,9 @@ endif ifndef BUILD_MISSIONPACK BUILD_MISSIONPACK= endif +ifndef BUILD_RENDERER_OPENGL1 + BUILD_RENDERER_OPENGL1= +endif ifndef BUILD_RENDERER_OPENGL2 BUILD_RENDERER_OPENGL2= endif @@ -1073,6 +1076,7 @@ ifeq ($(PLATFORM),emscripten) HAVE_VM_COMPILED=false BUILD_GAME_SO=0 BUILD_GAME_QVM=0 + BUILD_RENDERER_OPENGL1=0 # Would be interesting to try to get the server working via WebRTC DataChannel. # This would enable P2P play, hosting a server in the browser. Also, # DataChannel is the only way to use UDP in the browser. @@ -1178,16 +1182,26 @@ ifneq ($(BUILD_SERVER),0) endif ifneq ($(BUILD_CLIENT),0) - ifneq ($(PLATFORM),emscripten) + ifneq ($(USE_RENDERER_DLOPEN),0) TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT) - endif - ifneq ($(USE_RENDERER_DLOPEN),0) - TARGETS += $(B)/renderer_opengl1_$(SHLIBNAME) + ifneq ($(BUILD_RENDERER_OPENGL1),0) + TARGETS += $(B)/renderer_opengl1_$(SHLIBNAME) + endif ifneq ($(BUILD_RENDERER_OPENGL2),0) TARGETS += $(B)/renderer_opengl2_$(SHLIBNAME) endif else + ifneq ($(BUILD_RENDERER_OPENGL1),0) + TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT) + + ifeq ($(PLATFORM),emscripten) + EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN).wasm32.wasm + ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) + EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN).wasm32.data + endif + endif + endif ifneq ($(BUILD_RENDERER_OPENGL2),0) TARGETS += $(B)/$(CLIENTBIN)_opengl2$(FULLBINEXT) @@ -3109,13 +3123,18 @@ ifneq ($(BUILD_GAME_SO),0) endif ifneq ($(BUILD_CLIENT),0) - $(INSTALL) $(STRIP_FLAG) -m 0755 $(BR)/$(CLIENTBIN)$(FULLBINEXT) $(COPYBINDIR)/$(CLIENTBIN)$(FULLBINEXT) ifneq ($(USE_RENDERER_DLOPEN),0) + $(INSTALL) $(STRIP_FLAG) -m 0755 $(BR)/$(CLIENTBIN)$(FULLBINEXT) $(COPYBINDIR)/$(CLIENTBIN)$(FULLBINEXT) + ifneq ($(BUILD_RENDERER_OPENGL1),0) $(INSTALL) $(STRIP_FLAG) -m 0755 $(BR)/renderer_opengl1_$(SHLIBNAME) $(COPYBINDIR)/renderer_opengl1_$(SHLIBNAME) + endif ifneq ($(BUILD_RENDERER_OPENGL2),0) $(INSTALL) $(STRIP_FLAG) -m 0755 $(BR)/renderer_opengl2_$(SHLIBNAME) $(COPYBINDIR)/renderer_opengl2_$(SHLIBNAME) endif else + ifneq ($(BUILD_RENDERER_OPENGL1),0) + $(INSTALL) $(STRIP_FLAG) -m 0755 $(BR)/$(CLIENTBIN)$(FULLBINEXT) $(COPYBINDIR)/$(CLIENTBIN)$(FULLBINEXT) + endif ifneq ($(BUILD_RENDERER_OPENGL2),0) $(INSTALL) $(STRIP_FLAG) -m 0755 $(BR)/$(CLIENTBIN)_opengl2$(FULLBINEXT) $(COPYBINDIR)/$(CLIENTBIN)_opengl2$(FULLBINEXT) endif diff --git a/README.md b/README.md index 51b27a8b..5873d06b 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,8 @@ Makefile.local: SERVERBIN - rename 'ioq3ded' server binary CLIENTBIN - rename 'ioquake3' client binary USE_RENDERER_DLOPEN - build and use the renderer in a library + BUILD_RENDERER_OPENGL1 build the opengl1 client / renderer library + BUILD_RENDERER_OPENGL2 build the opengl2 client / renderer library USE_YACC - use yacc to update code/tools/lcc/lburg/gram.c BASEGAME - rename 'baseq3' BASEGAME_CFLAGS - custom CFLAGS for basegame From ab8f7eb0bdd17a893b5fd6133323b30828696e37 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 9 Jun 2024 22:12:15 -0500 Subject: [PATCH 28/34] Clean up emscripten in Makefile - Remove emscripten build steps from Makefile (see readme) - USE_CURL=0 has no effect - HAVE_VM_COMPILED=false has no effect - WebGL 1 and 2 are supported by the OpenGL2 renderer - FULL_ES2 (client-side arrays) isn't needed - Rename EMSCRIPTENOBJ to GENERATEDTARGETS and separate it from TARGETS so it's more readable. --- Makefile | 85 +++++++++++++++++++++++++++----------------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index 8704d982..0dfd8333 100644 --- a/Makefile +++ b/Makefile @@ -1056,47 +1056,34 @@ else # ifeq sunos ifeq ($(PLATFORM),emscripten) - # 1. Create "baseq3" directory in the same directory as this Makefile. - # 2. Copy pak[0-8].pk3 into the created "baseq3" directory. - # 3. Run `source "/path/to/emsdk_env.sh"` to add emcc to PATH. - # 4. Run `make PLATFORM=emscripten` - # 5. Serve the build/release-emscripten-wasm32/ioquake3_opengl2.{html,js,wasm,data} from a web server. - # 6. Load ioquake3_opengl2.html in a web browser. - ifneq ($(findstring /emcc,$(CC)),/emcc) CC=emcc endif ARCH=wasm32 BINEXT=.js - - # LDFLAGS+=-s MAIN_MODULE is needed for dlopen() in client/server but it causes compile errors + + # dlopen(), opengl1, and networking are not functional USE_RENDERER_DLOPEN=0 USE_OPENAL_DLOPEN=0 - USE_CURL=0 - HAVE_VM_COMPILED=false BUILD_GAME_SO=0 BUILD_GAME_QVM=0 BUILD_RENDERER_OPENGL1=0 - # Would be interesting to try to get the server working via WebRTC DataChannel. - # This would enable P2P play, hosting a server in the browser. Also, - # DataChannel is the only way to use UDP in the browser. BUILD_SERVER=0 CLIENT_EXTRA_FILES+=code/web/ioquake3.html CLIENT_CFLAGS+=-s USE_SDL=2 - CLIENT_LDFLAGS+=-s TOTAL_MEMORY=256mb + CLIENT_LDFLAGS+=-s TOTAL_MEMORY=256MB CLIENT_LDFLAGS+=-s STACK_SIZE=5MB - # Informing Emscripten which WebGL versions we support makes the JS bundle smaller and faster to load. - CLIENT_LDFLAGS+=-s MIN_WEBGL_VERSION=2 - CLIENT_LDFLAGS+=-s MAX_WEBGL_VERSION=2 - CLIENT_LDFLAGS+=-s FULL_ES2=1 + CLIENT_LDFLAGS+=-s MIN_WEBGL_VERSION=1 -s MAX_WEBGL_VERSION=2 + # The HTML file can use these functions to load extra files before the game starts. CLIENT_LDFLAGS+=-s EXPORTED_RUNTIME_METHODS=FS,addRunDependency,removeRunDependency CLIENT_LDFLAGS+=-s EXIT_RUNTIME=1 CLIENT_LDFLAGS+=-s EXPORT_ES6 CLIENT_LDFLAGS+=-s EXPORT_NAME=ioquake3 + # Game data files can be packaged by emcc into a .data file that lives next to the wasm bundle # and added to the virtual filesystem before the game starts. This requires the game data to be # present at build time and it can't be changed afterward. @@ -1172,13 +1159,6 @@ endif ifneq ($(BUILD_SERVER),0) TARGETS += $(B)/$(SERVERBIN)$(FULLBINEXT) - - ifeq ($(PLATFORM),emscripten) - EMSCRIPTENOBJ+=$(B)/$(SERVERBIN).wasm - ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) - EMSCRIPTENOBJ+=$(B)/$(SERVERBIN).data - endif - endif endif ifneq ($(BUILD_CLIENT),0) @@ -1194,23 +1174,9 @@ ifneq ($(BUILD_CLIENT),0) else ifneq ($(BUILD_RENDERER_OPENGL1),0) TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT) - - ifeq ($(PLATFORM),emscripten) - EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN).wasm32.wasm - ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) - EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN).wasm32.data - endif - endif endif ifneq ($(BUILD_RENDERER_OPENGL2),0) TARGETS += $(B)/$(CLIENTBIN)_opengl2$(FULLBINEXT) - - ifeq ($(PLATFORM),emscripten) - EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN)_opengl2.wasm32.wasm - ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) - EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN)_opengl2.wasm32.data - endif - endif endif endif endif @@ -1255,6 +1221,37 @@ ifneq ($(BUILD_AUTOUPDATER),0) TARGETS += $(B)/$(AUTOUPDATER_BIN) endif +ifeq ($(PLATFORM),emscripten) + ifneq ($(BUILD_SERVER),0) + GENERATEDTARGETS += $(B)/$(SERVERBIN).$(ARCH).wasm + ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) + GENERATEDTARGETS += $(B)/$(SERVERBIN).$(ARCH).data + endif + endif + + ifneq ($(BUILD_CLIENT),0) + ifneq ($(USE_RENDERER_DLOPEN),0) + GENERATEDTARGETS += $(B)/$(CLIENTBIN).$(ARCH).wasm + ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) + GENERATEDTARGETS += $(B)/$(CLIENTBIN).$(ARCH).data + endif + else + ifneq ($(BUILD_RENDERER_OPENGL1),0) + GENERATEDTARGETS += $(B)/$(CLIENTBIN).$(ARCH).wasm + ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) + GENERATEDTARGETS += $(B)/$(CLIENTBIN).$(ARCH).data + endif + endif + ifneq ($(BUILD_RENDERER_OPENGL2),0) + GENERATEDTARGETS += $(B)/$(CLIENTBIN)_opengl2.$(ARCH).wasm + ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) + GENERATEDTARGETS += $(B)/$(CLIENTBIN)_opengl2.$(ARCH).data + endif + endif + endif + endif +endif + ifeq ($(USE_OPENAL),1) CLIENT_CFLAGS += -DUSE_OPENAL ifeq ($(USE_OPENAL_DLOPEN),1) @@ -1558,7 +1555,7 @@ ifneq ($(BUILD_CLIENT),0) endif NAKED_TARGETS=$(shell echo $(TARGETS) | sed -e "s!$(B)/!!g") -NAKED_EMSCRIPTENOBJ=$(shell echo $(EMSCRIPTENOBJ) | sed -e "s!$(B)/!!g") +NAKED_GENERATEDTARGETS=$(shell echo $(GENERATEDTARGETS) | sed -e "s!$(B)/!!g") print_list=-@for i in $(1); \ do \ @@ -1614,7 +1611,7 @@ endif @echo "" @echo " Output:" $(call print_list, $(NAKED_TARGETS)) - $(call print_list, $(NAKED_EMSCRIPTENOBJ)) + $(call print_list, $(NAKED_GENERATEDTARGETS)) @echo "" ifeq ($(PLATFORM),emscripten) ifneq ($(EMSCRIPTEN_PRELOAD_FILE),1) @@ -1643,7 +1640,7 @@ endif ifneq ($(PLATFORM),darwin) ifdef ARCHIVE @rm -f $@ - @(cd $(B) && zip -r9 ../../$@ $(NAKED_TARGETS) $(NAKED_EMSCRIPTENOBJ)) + @(cd $(B) && zip -r9 ../../$@ $(NAKED_TARGETS) $(NAKED_GENERATEDTARGETS)) endif endif @: @@ -3184,8 +3181,8 @@ clean2: @rm -f $(OBJ) @rm -f $(OBJ_D_FILES) @rm -f $(STRINGOBJ) - @rm -f $(EMSCRIPTENOBJ) @rm -f $(TARGETS) + @rm -f $(GENERATEDTARGETS) toolsclean: toolsclean-debug toolsclean-release From 41ec9ae05f9f5d1b6ce1387f60d00727edfceeb5 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Mon, 10 Jun 2024 20:50:48 -0500 Subject: [PATCH 29/34] Make emscripten --preload-file opt-in --- Makefile | 18 ++++-------------- README.md | 13 ++++++++----- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index 0dfd8333..cdbef2ea 100644 --- a/Makefile +++ b/Makefile @@ -1090,9 +1090,11 @@ ifeq ($(PLATFORM),emscripten) # For more flexibility, game data files can be loaded from a web server at runtime by listing # them in ioq3-config.json. This way they don't have to be present at build time and can be # changed later. - ifneq ($(wildcard $(BASEGAME)/*),) + ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) + ifeq ($(wildcard $(BASEGAME)/*),) + $(error "No files in '$(BASEGAME)' directory for emscripten to preload.") + endif CLIENT_LDFLAGS+=--preload-file $(BASEGAME) - EMSCRIPTEN_PRELOAD_FILE=1 CLIENT_EXTRA_FILES+=code/web/empty/ioq3-config.json else CLIENT_EXTRA_FILES+=code/web/$(BASEGAME)/ioq3-config.json @@ -1613,18 +1615,6 @@ endif $(call print_list, $(NAKED_TARGETS)) $(call print_list, $(NAKED_GENERATEDTARGETS)) @echo "" -ifeq ($(PLATFORM),emscripten) -ifneq ($(EMSCRIPTEN_PRELOAD_FILE),1) - @echo " Warning: Game files not found in '$(BASEGAME)'." - @echo " They will not be packaged by Emscripten or preloaded." - @echo " To run this build you must serve the game files from a web server" - @echo " and list their paths in ioq3-config.json." - @echo " To make a build that automatically loads the game files, create a" - @echo " directory called '$(BASEGAME)' and copy your game files into it, then" - @echo " 'emmake make clean' and rebuild." - @echo "" -endif -endif ifneq ($(TARGETS),) ifndef DEBUG_MAKEFILE @$(MAKE) $(TARGETS) $(B).zip V=$(V) diff --git a/README.md b/README.md index 5873d06b..322c358d 100644 --- a/README.md +++ b/README.md @@ -102,13 +102,12 @@ For macOS, building a Universal Binary 2 (macOS 10.9+, arm64, x86_64) For Web, building with Emscripten 1. Follow the installation instructions for the Emscripten SDK including setting up the environment with emsdk_env. - 2. Copy or symlink your baseq3 directory into this directory so Emscripten - can package your game files. (Alternatively, you can build without this, - and provide the game data files at runtime. For this, game files should - be listed in `ioq3-config.json`) - 3. Run `emmake make debug` (or release, but if you do both then you will + 2. Run `emmake make debug` (or release, but if you do both then you will need to pass an extra URL parameter to the HTML file to select the build you want). + 3. Copy or symlink your baseq3 pk3 files into the `build/debug-emscripten-wasm32/baseq3` + directory so they can be loaded at run-time. Only game files listed in + `ioq3-config.json` will be loaded. 4. Start a web server serving this directory. `python3 -m http.server` is an easy default that you may already have installed. 5. Open `code/web/ioquake3.html` in a web browser. Open the developer @@ -177,6 +176,10 @@ Makefile.local: DEBUG_CFLAGS - C compiler flags to use for building debug version COPYDIR - the target installation directory TEMPDIR - specify user defined directory for temp files + EMSCRIPTEN_PRELOAD_FILE - set to 1 to package 'baseq3' (BASEGAME) directory + containing pk3s and loose files as a single + .data file that is loaded instead of listing + individual files in ioq3-config.json ``` The defaults for these variables differ depending on the target platform. From e6c0776d98d5a0a53279495d16cbfe54037f0ebe Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Mon, 10 Jun 2024 02:55:20 -0500 Subject: [PATCH 30/34] Allow web client to use unzipped QVMs Compile the QVMs and automatically uses them when not using --preload-file. --- Makefile | 1 - code/web/baseq3/ioq3-config.json | 5 ++++- code/web/ioquake3.html | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index cdbef2ea..716500ea 100644 --- a/Makefile +++ b/Makefile @@ -1066,7 +1066,6 @@ ifeq ($(PLATFORM),emscripten) USE_RENDERER_DLOPEN=0 USE_OPENAL_DLOPEN=0 BUILD_GAME_SO=0 - BUILD_GAME_QVM=0 BUILD_RENDERER_OPENGL1=0 BUILD_SERVER=0 diff --git a/code/web/baseq3/ioq3-config.json b/code/web/baseq3/ioq3-config.json index b0cb1f16..1ee8cc01 100644 --- a/code/web/baseq3/ioq3-config.json +++ b/code/web/baseq3/ioq3-config.json @@ -8,6 +8,9 @@ {"src": "baseq3/pak5.pk3", "dst": "/baseq3"}, {"src": "baseq3/pak6.pk3", "dst": "/baseq3"}, {"src": "baseq3/pak7.pk3", "dst": "/baseq3"}, - {"src": "baseq3/pak8.pk3", "dst": "/baseq3"} + {"src": "baseq3/pak8.pk3", "dst": "/baseq3"}, + {"src": "baseq3/vm/cgame.qvm", "dst": "/baseq3/vm"}, + {"src": "baseq3/vm/qagame.qvm", "dst": "/baseq3/vm"}, + {"src": "baseq3/vm/ui.qvm", "dst": "/baseq3/vm"} ] } diff --git a/code/web/ioquake3.html b/code/web/ioquake3.html index 4aac449d..93923cd5 100644 --- a/code/web/ioquake3.html +++ b/code/web/ioquake3.html @@ -14,6 +14,7 @@ const urlParams = new URLSearchParams(window.location.search); const basegame = urlParams.get('basegame') || 'baseq3'; let generatedArguments = ` + +set sv_pure 0 +set net_enabled 0 +set r_mode -2 +set fs_game ${basegame} From 8365ea7ed29876f8c42785c517338c12127f390d Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 9 Jun 2024 23:24:12 -0500 Subject: [PATCH 31/34] Customize the web client HTML file Modify the client HTML file when copying it to the build directory to apply current CLIENTBIN and BASEGAME. It always loads engine/data from the current directory (no need to try to locate the build directory). --- Makefile | 14 +++++- README.md | 8 ++-- code/web/client.html | 96 ++++++++++++++++++++++++++++++++++++++++++ code/web/ioquake3.html | 71 ------------------------------- 4 files changed, 111 insertions(+), 78 deletions(-) create mode 100644 code/web/client.html delete mode 100644 code/web/ioquake3.html diff --git a/Makefile b/Makefile index 716500ea..19b2df83 100644 --- a/Makefile +++ b/Makefile @@ -291,6 +291,7 @@ LIBTOMCRYPTSRCDIR=$(AUTOUPDATERSRCDIR)/rsa_tools/libtomcrypt-1.17 TOMSFASTMATHSRCDIR=$(AUTOUPDATERSRCDIR)/rsa_tools/tomsfastmath-0.13.1 LOKISETUPDIR=misc/setup NSISDIR=misc/nsis +WEBDIR=$(MOUNT_DIR)/web SDLHDIR=$(MOUNT_DIR)/SDL2 LIBSDIR=$(MOUNT_DIR)/libs @@ -1069,8 +1070,6 @@ ifeq ($(PLATFORM),emscripten) BUILD_RENDERER_OPENGL1=0 BUILD_SERVER=0 - CLIENT_EXTRA_FILES+=code/web/ioquake3.html - CLIENT_CFLAGS+=-s USE_SDL=2 CLIENT_LDFLAGS+=-s TOTAL_MEMORY=256MB @@ -1231,6 +1230,8 @@ ifeq ($(PLATFORM),emscripten) endif ifneq ($(BUILD_CLIENT),0) + TARGETS += $(B)/$(CLIENTBIN).html + ifneq ($(USE_RENDERER_DLOPEN),0) GENERATEDTARGETS += $(B)/$(CLIENTBIN).$(ARCH).wasm ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) @@ -3086,6 +3087,15 @@ $(B)/$(MISSIONPACK)/qcommon/%.asm: $(CMDIR)/%.c $(Q3LCC) $(DO_Q3LCC_MISSIONPACK) +############################################################################# +# EMSCRIPTEN +############################################################################# + +$(B)/$(CLIENTBIN).html: $(WEBDIR)/client.html + $(echo_cmd) "SED $@" + $(Q)sed 's/__CLIENTBIN__/$(CLIENTBIN)/g;s/__BASEGAME__/$(BASEGAME)/g' < $< > $@ + + ############################################################################# # MISC ############################################################################# diff --git a/README.md b/README.md index 322c358d..dd374263 100644 --- a/README.md +++ b/README.md @@ -102,16 +102,14 @@ For macOS, building a Universal Binary 2 (macOS 10.9+, arm64, x86_64) For Web, building with Emscripten 1. Follow the installation instructions for the Emscripten SDK including setting up the environment with emsdk_env. - 2. Run `emmake make debug` (or release, but if you do both then you will - need to pass an extra URL parameter to the HTML file to select the - build you want). + 2. Run `emmake make debug` (or release). 3. Copy or symlink your baseq3 pk3 files into the `build/debug-emscripten-wasm32/baseq3` directory so they can be loaded at run-time. Only game files listed in `ioq3-config.json` will be loaded. 4. Start a web server serving this directory. `python3 -m http.server` is an easy default that you may already have installed. - 5. Open `code/web/ioquake3.html` in a web browser. Open the developer - console to see errors and warnings. + 5. Open `http://localhost:8000/build/debug-emscripten-wasm32/ioquake3.html` + in a web browser. Open the developer console to see errors and warnings. 6. Debugging the C code is possible using a Chrome extension. For details see https://developer.chrome.com/blog/wasm-debugging-2020 diff --git a/code/web/client.html b/code/web/client.html new file mode 100644 index 00000000..3814ad6c --- /dev/null +++ b/code/web/client.html @@ -0,0 +1,96 @@ + +__CLIENTBIN__ Emscripten demo + + + + + diff --git a/code/web/ioquake3.html b/code/web/ioquake3.html deleted file mode 100644 index 93923cd5..00000000 --- a/code/web/ioquake3.html +++ /dev/null @@ -1,71 +0,0 @@ - -ioquake3 Emscripten demo - - - - - From 2660bb4a03213a430b68f22d1f1b08aaff8004ee Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 9 Jun 2024 22:28:43 -0500 Subject: [PATCH 32/34] Add support for overriding basegame to web client List files for multiple games in a single client-config.json file so that com_basegame argument can pick different game data. Use ioquake3.html?com_basegame=demoq3 (or tademo) to run the Quake 3 or Team Arena demo. They require new QVMs from baseq3/missionpack to run. --- Makefile | 7 +++---- README.md | 4 ++-- code/web/baseq3/ioq3-config.json | 16 -------------- code/web/client-config.json | 36 ++++++++++++++++++++++++++++++++ code/web/client.html | 27 ++++++++++++++++-------- code/web/empty/ioq3-config.json | 3 --- 6 files changed, 59 insertions(+), 34 deletions(-) delete mode 100644 code/web/baseq3/ioq3-config.json create mode 100644 code/web/client-config.json delete mode 100644 code/web/empty/ioq3-config.json diff --git a/Makefile b/Makefile index 19b2df83..fe8b3709 100644 --- a/Makefile +++ b/Makefile @@ -1086,16 +1086,15 @@ ifeq ($(PLATFORM),emscripten) # and added to the virtual filesystem before the game starts. This requires the game data to be # present at build time and it can't be changed afterward. # For more flexibility, game data files can be loaded from a web server at runtime by listing - # them in ioq3-config.json. This way they don't have to be present at build time and can be + # them in client-config.json. This way they don't have to be present at build time and can be # changed later. ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1) ifeq ($(wildcard $(BASEGAME)/*),) $(error "No files in '$(BASEGAME)' directory for emscripten to preload.") endif CLIENT_LDFLAGS+=--preload-file $(BASEGAME) - CLIENT_EXTRA_FILES+=code/web/empty/ioq3-config.json else - CLIENT_EXTRA_FILES+=code/web/$(BASEGAME)/ioq3-config.json + CLIENT_EXTRA_FILES+=code/web/client-config.json endif OPTIMIZEVM = -O3 @@ -3093,7 +3092,7 @@ $(B)/$(MISSIONPACK)/qcommon/%.asm: $(CMDIR)/%.c $(Q3LCC) $(B)/$(CLIENTBIN).html: $(WEBDIR)/client.html $(echo_cmd) "SED $@" - $(Q)sed 's/__CLIENTBIN__/$(CLIENTBIN)/g;s/__BASEGAME__/$(BASEGAME)/g' < $< > $@ + $(Q)sed 's/__CLIENTBIN__/$(CLIENTBIN)/g;s/__BASEGAME__/$(BASEGAME)/g;s/__EMSCRIPTEN_PRELOAD_FILE__/$(EMSCRIPTEN_PRELOAD_FILE)/g' < $< > $@ ############################################################################# diff --git a/README.md b/README.md index dd374263..bbaede21 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ For Web, building with Emscripten 2. Run `emmake make debug` (or release). 3. Copy or symlink your baseq3 pk3 files into the `build/debug-emscripten-wasm32/baseq3` directory so they can be loaded at run-time. Only game files listed in - `ioq3-config.json` will be loaded. + `client-config.json` will be loaded. 4. Start a web server serving this directory. `python3 -m http.server` is an easy default that you may already have installed. 5. Open `http://localhost:8000/build/debug-emscripten-wasm32/ioquake3.html` @@ -177,7 +177,7 @@ Makefile.local: EMSCRIPTEN_PRELOAD_FILE - set to 1 to package 'baseq3' (BASEGAME) directory containing pk3s and loose files as a single .data file that is loaded instead of listing - individual files in ioq3-config.json + individual files in client-config.json ``` The defaults for these variables differ depending on the target platform. diff --git a/code/web/baseq3/ioq3-config.json b/code/web/baseq3/ioq3-config.json deleted file mode 100644 index 1ee8cc01..00000000 --- a/code/web/baseq3/ioq3-config.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "files": [ - {"src": "baseq3/pak0.pk3", "dst": "/baseq3"}, - {"src": "baseq3/pak1.pk3", "dst": "/baseq3"}, - {"src": "baseq3/pak2.pk3", "dst": "/baseq3"}, - {"src": "baseq3/pak3.pk3", "dst": "/baseq3"}, - {"src": "baseq3/pak4.pk3", "dst": "/baseq3"}, - {"src": "baseq3/pak5.pk3", "dst": "/baseq3"}, - {"src": "baseq3/pak6.pk3", "dst": "/baseq3"}, - {"src": "baseq3/pak7.pk3", "dst": "/baseq3"}, - {"src": "baseq3/pak8.pk3", "dst": "/baseq3"}, - {"src": "baseq3/vm/cgame.qvm", "dst": "/baseq3/vm"}, - {"src": "baseq3/vm/qagame.qvm", "dst": "/baseq3/vm"}, - {"src": "baseq3/vm/ui.qvm", "dst": "/baseq3/vm"} - ] -} diff --git a/code/web/client-config.json b/code/web/client-config.json new file mode 100644 index 00000000..3dc05b98 --- /dev/null +++ b/code/web/client-config.json @@ -0,0 +1,36 @@ +{ + "baseq3": { + "files": [ + {"src": "baseq3/pak0.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak1.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak2.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak3.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak4.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak5.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak6.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak7.pk3", "dst": "/baseq3"}, + {"src": "baseq3/pak8.pk3", "dst": "/baseq3"}, + {"src": "baseq3/vm/cgame.qvm", "dst": "/baseq3/vm"}, + {"src": "baseq3/vm/qagame.qvm", "dst": "/baseq3/vm"}, + {"src": "baseq3/vm/ui.qvm", "dst": "/baseq3/vm"} + ] + }, + "demoq3": { + "_comment": "Copy baseq3/vm/*.qvm to demoq3/vm/ as the Quake 3 demo QVMs are not compatible. However the botfiles are not fully compatible with newer QVMs.", + "files": [ + {"src": "demoq3/pak0.pk3", "dst": "/demoq3"}, + {"src": "demoq3/vm/cgame.qvm", "dst": "/demoq3/vm"}, + {"src": "demoq3/vm/qagame.qvm", "dst": "/demoq3/vm"}, + {"src": "demoq3/vm/ui.qvm", "dst": "/demoq3/vm"} + ] + }, + "tademo": { + "_comment": "Copy missionpack/vm/*.qvm to tademo/vm/ as the Team Arena demo QVMs are not compatible.", + "files": [ + {"src": "tademo/pak0.pk3", "dst": "/tademo"}, + {"src": "tademo/vm/cgame.qvm", "dst": "/tademo/vm"}, + {"src": "tademo/vm/qagame.qvm", "dst": "/tademo/vm"}, + {"src": "tademo/vm/ui.qvm", "dst": "/tademo/vm"} + ] + } +} diff --git a/code/web/client.html b/code/web/client.html index 3814ad6c..a354ad2c 100644 --- a/code/web/client.html +++ b/code/web/client.html @@ -11,6 +11,7 @@ // These strings are set in the generated HTML file in the build directory. let CLIENTBIN = '__CLIENTBIN__'; let BASEGAME = '__BASEGAME__'; +let EMSCRIPTEN_PRELOAD_FILE = Number('__EMSCRIPTEN_PRELOAD_FILE__'); // Detect if it's not the generated HTML file. let clientHtmlFallback = (CLIENTBIN === '\_\_CLIENTBIN\_\_'); @@ -19,24 +20,25 @@ // Path or URL containing fs_game directories. let dataPath = './'; // Path or URL for config file that specifies the files to load for each fs_game. -let configFilename = './ioq3-config.json'; +let configFilename = './client-config.json'; // If displaying the unmodified HTML file, fallback to defaults. if (clientHtmlFallback) { CLIENTBIN='ioquake3'; BASEGAME='baseq3'; + EMSCRIPTEN_PRELOAD_FILE=0; } if (window.location.protocol === 'file:') throw new Error(`Unfortunately browser security restrictions prevent loading wasm from a file: URL. This file must be loaded from a web server. The easiest way to do this is probably to use Python\'s built-in web server by running \`python3 -m http.server\` in the top level source directory and then navigate to http://localhost:8000/build/debug-emscripten-wasm32/${CLIENTBIN}.html`); // First set up the command line arguments and the Emscripten filesystem. const urlParams = new URLSearchParams(window.location.search); -const basegame = urlParams.get('basegame') || BASEGAME; +const com_basegame = urlParams.get('com_basegame') || BASEGAME; let generatedArguments = ` +set sv_pure 0 +set net_enabled 0 +set r_mode -2 - +set fs_game ${basegame} + +set com_basegame "${com_basegame}" `; // Note that unfortunately "+" needs to be encoded as "%2b" in URL query strings or it will be stripped by the browser. const queryArgs = urlParams.get('args'); @@ -62,12 +64,13 @@ enginePath = buildPaths[buildIndex]; dataPath = buildPaths[buildIndex]; - configFilename = dataPath + 'ioq3-config.json'; + configFilename = dataPath + 'client-config.json'; } const dataURL = new URL(dataPath, location.origin + location.pathname); -const configPromise = fetch(configFilename).then(r => r.ok ? r.json() : {files: []}); +const configPromise = ( EMSCRIPTEN_PRELOAD_FILE === 1 ) ? Promise.resolve({[BASEGAME]: {files: []}}) + : fetch(configFilename).then(r => r.ok ? r.json() : { /* empty config */ }); const ioquake3 = (await import(enginePath + `${CLIENTBIN}_opengl2.wasm32.js`)).default; ioquake3({ @@ -78,13 +81,19 @@ module.addRunDependency('setup-ioq3-filesystem'); try { const config = await configPromise; - const fetches = config.files.map(file => fetch(new URL(file.src, dataURL))); - for (let i = 0; i < config.files.length; i++) { + const gamedir = com_basegame; + if (config[gamedir] === null + || config[gamedir].files === null) { + console.warn(`Game directory '${gamedir}' cannot be used. It must have files listed in ${configFilename}.`); + } + const files = config[gamedir].files; + const fetches = files.map(file => fetch(new URL(file.src, dataURL))); + for (let i = 0; i < files.length; i++) { const response = await fetches[i]; if (!response.ok) continue; const data = await response.arrayBuffer(); - let name = config.files[i].src.match(/[^/]+$/)[0]; - let dir = config.files[i].dst; + let name = files[i].src.match(/[^/]+$/)[0]; + let dir = files[i].dst; module.FS.mkdirTree(dir); module.FS.writeFile(`${dir}/${name}`, new Uint8Array(data)); } diff --git a/code/web/empty/ioq3-config.json b/code/web/empty/ioq3-config.json deleted file mode 100644 index 158547c2..00000000 --- a/code/web/empty/ioq3-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "files": [] -} From 7bfd5c90b832ec5816ecbde623214b57bb121484 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Mon, 10 Jun 2024 01:29:34 -0500 Subject: [PATCH 33/34] Add support for mods to web client ioquake3.html?fs_game=missionpack will run Team Arena when not using --preload-file. --- code/web/client-config.json | 11 ++++++++++ code/web/client.html | 41 +++++++++++++++++++++++-------------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/code/web/client-config.json b/code/web/client-config.json index 3dc05b98..eddf35db 100644 --- a/code/web/client-config.json +++ b/code/web/client-config.json @@ -15,6 +15,17 @@ {"src": "baseq3/vm/ui.qvm", "dst": "/baseq3/vm"} ] }, + "missionpack": { + "files": [ + {"src": "missionpack/pak0.pk3", "dst": "/missionpack"}, + {"src": "missionpack/pak1.pk3", "dst": "/missionpack"}, + {"src": "missionpack/pak2.pk3", "dst": "/missionpack"}, + {"src": "missionpack/pak3.pk3", "dst": "/missionpack"}, + {"src": "missionpack/vm/cgame.qvm", "dst": "/missionpack/vm"}, + {"src": "missionpack/vm/qagame.qvm", "dst": "/missionpack/vm"}, + {"src": "missionpack/vm/ui.qvm", "dst": "/missionpack/vm"} + ] + }, "demoq3": { "_comment": "Copy baseq3/vm/*.qvm to demoq3/vm/ as the Quake 3 demo QVMs are not compatible. However the botfiles are not fully compatible with newer QVMs.", "files": [ diff --git a/code/web/client.html b/code/web/client.html index a354ad2c..a1cf00fa 100644 --- a/code/web/client.html +++ b/code/web/client.html @@ -34,11 +34,15 @@ // First set up the command line arguments and the Emscripten filesystem. const urlParams = new URLSearchParams(window.location.search); const com_basegame = urlParams.get('com_basegame') || BASEGAME; +const fs_basegame = urlParams.get('fs_basegame') || ''; +const fs_game = urlParams.get('fs_game') || ''; let generatedArguments = ` +set sv_pure 0 +set net_enabled 0 +set r_mode -2 +set com_basegame "${com_basegame}" + +set fs_basegame "${fs_basegame}" + +set fs_game "${fs_game}" `; // Note that unfortunately "+" needs to be encoded as "%2b" in URL query strings or it will be stripped by the browser. const queryArgs = urlParams.get('args'); @@ -81,21 +85,28 @@ module.addRunDependency('setup-ioq3-filesystem'); try { const config = await configPromise; - const gamedir = com_basegame; - if (config[gamedir] === null - || config[gamedir].files === null) { - console.warn(`Game directory '${gamedir}' cannot be used. It must have files listed in ${configFilename}.`); - } - const files = config[gamedir].files; - const fetches = files.map(file => fetch(new URL(file.src, dataURL))); - for (let i = 0; i < files.length; i++) { - const response = await fetches[i]; - if (!response.ok) continue; - const data = await response.arrayBuffer(); - let name = files[i].src.match(/[^/]+$/)[0]; - let dir = files[i].dst; - module.FS.mkdirTree(dir); - module.FS.writeFile(`${dir}/${name}`, new Uint8Array(data)); + const gamedirs = [com_basegame,fs_basegame,fs_game]; + for (let g = 0; g < gamedirs.length; g++) { + const gamedir = gamedirs[g]; + if (gamedir === '') { + continue; + } + if (config[gamedir] === null + || config[gamedir].files === null) { + console.warn(`Game directory '${gamedir}' cannot be used. It must have files listed in ${configFilename}.`); + continue; + } + const files = config[gamedir].files; + const fetches = files.map(file => fetch(new URL(file.src, dataURL))); + for (let i = 0; i < files.length; i++) { + const response = await fetches[i]; + if (!response.ok) continue; + const data = await response.arrayBuffer(); + let name = files[i].src.match(/[^/]+$/)[0]; + let dir = files[i].dst; + module.FS.mkdirTree(dir); + module.FS.writeFile(`${dir}/${name}`, new Uint8Array(data)); + } } } finally { module.removeRunDependency('setup-ioq3-filesystem'); From c1ab47a7649bed3a9d626833f661fbc6768c1b87 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Mon, 10 Jun 2024 03:14:12 -0500 Subject: [PATCH 34/34] Rename client-config.json based on CLIENTBIN Also make viewing client.html directly use client-config.json in the same directory so it's possible to "live edit" both the files without having to run make. --- Makefile | 9 +++++++-- code/web/client.html | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index fe8b3709..1dc0abd2 100644 --- a/Makefile +++ b/Makefile @@ -1093,8 +1093,6 @@ ifeq ($(PLATFORM),emscripten) $(error "No files in '$(BASEGAME)' directory for emscripten to preload.") endif CLIENT_LDFLAGS+=--preload-file $(BASEGAME) - else - CLIENT_EXTRA_FILES+=code/web/client-config.json endif OPTIMIZEVM = -O3 @@ -1230,6 +1228,9 @@ ifeq ($(PLATFORM),emscripten) ifneq ($(BUILD_CLIENT),0) TARGETS += $(B)/$(CLIENTBIN).html + ifneq ($(EMSCRIPTEN_PRELOAD_FILE),1) + TARGETS += $(B)/$(CLIENTBIN)-config.json + endif ifneq ($(USE_RENDERER_DLOPEN),0) GENERATEDTARGETS += $(B)/$(CLIENTBIN).$(ARCH).wasm @@ -3094,6 +3095,10 @@ $(B)/$(CLIENTBIN).html: $(WEBDIR)/client.html $(echo_cmd) "SED $@" $(Q)sed 's/__CLIENTBIN__/$(CLIENTBIN)/g;s/__BASEGAME__/$(BASEGAME)/g;s/__EMSCRIPTEN_PRELOAD_FILE__/$(EMSCRIPTEN_PRELOAD_FILE)/g' < $< > $@ +$(B)/$(CLIENTBIN)-config.json: $(WEBDIR)/client-config.json + $(echo_cmd) "CP $@" + $(Q)cp $< $@ + ############################################################################# # MISC diff --git a/code/web/client.html b/code/web/client.html index a1cf00fa..3a23ab92 100644 --- a/code/web/client.html +++ b/code/web/client.html @@ -20,13 +20,14 @@ // Path or URL containing fs_game directories. let dataPath = './'; // Path or URL for config file that specifies the files to load for each fs_game. -let configFilename = './client-config.json'; +let configFilename = `./${CLIENTBIN}-config.json`; // If displaying the unmodified HTML file, fallback to defaults. if (clientHtmlFallback) { CLIENTBIN='ioquake3'; BASEGAME='baseq3'; EMSCRIPTEN_PRELOAD_FILE=0; + configFilename='./client-config.json'; } if (window.location.protocol === 'file:') throw new Error(`Unfortunately browser security restrictions prevent loading wasm from a file: URL. This file must be loaded from a web server. The easiest way to do this is probably to use Python\'s built-in web server by running \`python3 -m http.server\` in the top level source directory and then navigate to http://localhost:8000/build/debug-emscripten-wasm32/${CLIENTBIN}.html`); @@ -68,7 +69,6 @@ enginePath = buildPaths[buildIndex]; dataPath = buildPaths[buildIndex]; - configFilename = dataPath + 'client-config.json'; } const dataURL = new URL(dataPath, location.origin + location.pathname);