diff --git a/src/core/audio/alut_web.h b/src/core/audio/alut_web.h new file mode 100644 index 00000000..0ac1ea69 --- /dev/null +++ b/src/core/audio/alut_web.h @@ -0,0 +1,111 @@ +/* +Minimal replacement for the five freealut calls AstroMenace uses, for the +Emscripten build: Emscripten ships OpenAL but no ALUT. Only reads what the game +data has, PCM WAV files with 8 or 16 bits per sample, mono or stereo. + +Written for the FredyGames build, released under the same license as +AstroMenace (GNU General Public License version 3 or later). +*/ + +#ifndef CORE_AUDIO_ALUT_WEB_H +#define CORE_AUDIO_ALUT_WEB_H + +#include +#include + +#define ALUT_ERROR_NO_ERROR 0 +#define ALUT_ERROR_UNSUPPORTED_FILE_TYPE 0x20A +#define ALUT_ERROR_CORRUPT_OR_TRUNCATED_DATA 0x20D + +static ALenum alutWebError = ALUT_ERROR_NO_ERROR; + +static inline ALboolean alutInitWithoutContext(int *, char **) { return AL_TRUE; } +static inline ALboolean alutExit() { return AL_TRUE; } +static inline ALint alutGetMajorVersion() { return 1; } +static inline ALint alutGetMinorVersion() { return 1; } + +static inline ALenum alutGetError() +{ + ALenum Error = alutWebError; + alutWebError = ALUT_ERROR_NO_ERROR; + return Error; +} + +static inline const char *alutGetErrorString(ALenum Error) +{ + switch (Error) { + case ALUT_ERROR_NO_ERROR: + return "No ALUT error found"; + case ALUT_ERROR_UNSUPPORTED_FILE_TYPE: + return "Unsupported file type"; + default: + return "Corrupt or truncated data"; + } +} + +static inline uint32_t alutWebLE(const uint8_t *Data, int Bytes) +{ + uint32_t Value = 0; + for (int i = Bytes - 1; i >= 0; i--) { + Value = (Value << 8) | Data[i]; + } + return Value; +} + +static inline ALuint alutCreateBufferFromFileImage(const ALvoid *Image, ALsizei Size) +{ + const uint8_t *Data = static_cast(Image); + if (Size < 12 || memcmp(Data, "RIFF", 4) || memcmp(Data + 8, "WAVE", 4)) { + alutWebError = ALUT_ERROR_UNSUPPORTED_FILE_TYPE; + return 0; + } + + uint32_t Channels = 0, Rate = 0, Bits = 0; + const uint8_t *Samples = nullptr; + uint32_t SamplesSize = 0; + for (ALsizei Pos = 12; Pos + 8 <= Size;) { + uint32_t ChunkSize = alutWebLE(Data + Pos + 4, 4); + const uint8_t *Chunk = Data + Pos + 8; + if (ChunkSize > static_cast(Size - Pos - 8)) { + ChunkSize = static_cast(Size - Pos - 8); + } + if (!memcmp(Data + Pos, "fmt ", 4) && ChunkSize >= 16) { + if (alutWebLE(Chunk, 2) != 1) { // PCM only + alutWebError = ALUT_ERROR_UNSUPPORTED_FILE_TYPE; + return 0; + } + Channels = alutWebLE(Chunk + 2, 2); + Rate = alutWebLE(Chunk + 4, 4); + Bits = alutWebLE(Chunk + 14, 2); + } else if (!memcmp(Data + Pos, "data", 4)) { + Samples = Chunk; + SamplesSize = ChunkSize; + } + Pos += 8 + ChunkSize + (ChunkSize & 1); + } + + ALenum Format; + if (Channels == 1 && Bits == 8) { + Format = AL_FORMAT_MONO8; + } else if (Channels == 1 && Bits == 16) { + Format = AL_FORMAT_MONO16; + } else if (Channels == 2 && Bits == 8) { + Format = AL_FORMAT_STEREO8; + } else if (Channels == 2 && Bits == 16) { + Format = AL_FORMAT_STEREO16; + } else { + alutWebError = ALUT_ERROR_UNSUPPORTED_FILE_TYPE; + return 0; + } + if (!Samples || !Rate) { + alutWebError = ALUT_ERROR_CORRUPT_OR_TRUNCATED_DATA; + return 0; + } + + ALuint Buffer = 0; + alGenBuffers(1, &Buffer); + alBufferData(Buffer, Format, Samples, static_cast(SamplesSize), static_cast(Rate)); + return Buffer; +} + +#endif // CORE_AUDIO_ALUT_WEB_H diff --git a/src/core/audio/openal.h b/src/core/audio/openal.h index 1204130f..c617143a 100644 --- a/src/core/audio/openal.h +++ b/src/core/audio/openal.h @@ -44,7 +44,11 @@ sources, you are free to change or add methods to sound.h header file instead. #include "AL/al.h" #include "AL/alc.h" #endif // defined(__APPLE__) && defined(__MACH__) +#ifdef __EMSCRIPTEN__ +#include "alut_web.h" +#else #include "AL/alut.h" +#endif // __EMSCRIPTEN__ namespace viewizard { diff --git a/src/core/graphics/extensions.cpp b/src/core/graphics/extensions.cpp index f2202385..2eaac181 100644 --- a/src/core/graphics/extensions.cpp +++ b/src/core/graphics/extensions.cpp @@ -29,6 +29,12 @@ #include "opengl.h" #include "SDL2/SDL.h" +#ifdef __EMSCRIPTEN__ +// GL 1.x is emulated by gl4es on top of WebGL, so the functions come from gl4es, not from the browser. +#include +#define SDL_GL_GetProcAddress gl4es_GetProcAddress +#endif // __EMSCRIPTEN__ + namespace viewizard { // OpenGL 1.3 (only what we need or would need in future) diff --git a/src/core/graphics/gl_main.cpp b/src/core/graphics/gl_main.cpp index cd489ac5..d0863927 100644 --- a/src/core/graphics/gl_main.cpp +++ b/src/core/graphics/gl_main.cpp @@ -48,6 +48,11 @@ #include "SDL2/SDL.h" #include +#ifdef __EMSCRIPTEN__ +#include +#include +#endif // __EMSCRIPTEN__ + namespace viewizard { namespace { @@ -157,6 +162,12 @@ bool vw_CreateOpenGLContext(int VSync) GLContext = SDL_GL_CreateContext(SDLWindow); +#ifdef __EMSCRIPTEN__ + if (GLContext) { + initialize_gl4es(); + } +#endif // __EMSCRIPTEN__ + if (!GLContext) { std::cerr << __func__ << "(): " << "SDL_GL_CreateContext() failed: " << SDL_GetError() << "\n"; std::cerr << __func__ << "(): " << "Can't create OpenGL context.\n"; @@ -397,6 +408,11 @@ void vw_EndRendering() assert(SDLWindow); SDL_GL_SwapWindow(SDLWindow); + +#ifdef __EMSCRIPTEN__ + // give the frame to the browser (the game keeps its own loops, so the build uses Asyncify) + emscripten_sleep(0); +#endif // __EMSCRIPTEN__ } /* diff --git a/src/core/graphics/gl_texture.cpp b/src/core/graphics/gl_texture.cpp index 945777d5..6182803e 100644 --- a/src/core/graphics/gl_texture.cpp +++ b/src/core/graphics/gl_texture.cpp @@ -119,7 +119,9 @@ GLtexture vw_BuildTexture(const std::unique_ptr &PixelsArray, glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP_SGIS, GL_TRUE); glTexImage2D(GL_TEXTURE_2D, 0, InternalFormat, Width, Height, 0, Format, GL_UNSIGNED_BYTE, PixelsArray.get()); } else { +#ifndef __EMSCRIPTEN__ // no GLU on the web, and gl4es always has glGenerateMipmap gluBuild2DMipmaps(GL_TEXTURE_2D, InternalFormat, Width, Height, Format, GL_UNSIGNED_BYTE, PixelsArray.get()); +#endif // __EMSCRIPTEN__ } } else { if (pfn_glTexStorage2D) { diff --git a/src/core/graphics/glu_web.cpp b/src/core/graphics/glu_web.cpp new file mode 100644 index 00000000..bff86a44 --- /dev/null +++ b/src/core/graphics/glu_web.cpp @@ -0,0 +1,52 @@ +/* +The two GLU calls AstroMenace uses, gluPerspective() and gluLookAt(), for the +Emscripten build: gl4es emulates OpenGL but not GLU. Same math as the GLU +specification, applied to the current matrix. + +Written for the FredyGames build, released under the same license as +AstroMenace (GNU General Public License version 3 or later). +*/ + +#ifdef __EMSCRIPTEN__ + +#include "opengl.h" +#include + +void gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar) +{ + const GLdouble f = 1.0 / std::tan(fovy * M_PI / 360.0); + const GLfloat m[16]{ + static_cast(f / aspect), 0.0f, 0.0f, 0.0f, + 0.0f, static_cast(f), 0.0f, 0.0f, + 0.0f, 0.0f, static_cast((zFar + zNear) / (zNear - zFar)), -1.0f, + 0.0f, 0.0f, static_cast(2.0 * zFar * zNear / (zNear - zFar)), 0.0f}; + glMultMatrixf(m); +} + +void gluLookAt(GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ, + GLdouble centerX, GLdouble centerY, GLdouble centerZ, + GLdouble upX, GLdouble upY, GLdouble upZ) +{ + GLdouble fx = centerX - eyeX, fy = centerY - eyeY, fz = centerZ - eyeZ; + GLdouble len = std::sqrt(fx * fx + fy * fy + fz * fz); + if (len > 0.0) { + fx /= len; fy /= len; fz /= len; + } + // s = f x up, normalized + GLdouble sx = fy * upZ - fz * upY, sy = fz * upX - fx * upZ, sz = fx * upY - fy * upX; + len = std::sqrt(sx * sx + sy * sy + sz * sz); + if (len > 0.0) { + sx /= len; sy /= len; sz /= len; + } + // u = s x f + const GLdouble ux = sy * fz - sz * fy, uy = sz * fx - sx * fz, uz = sx * fy - sy * fx; + const GLfloat m[16]{ + static_cast(sx), static_cast(ux), static_cast(-fx), 0.0f, + static_cast(sy), static_cast(uy), static_cast(-fy), 0.0f, + static_cast(sz), static_cast(uz), static_cast(-fz), 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f}; + glMultMatrixf(m); + glTranslatef(static_cast(-eyeX), static_cast(-eyeY), static_cast(-eyeZ)); +} + +#endif // __EMSCRIPTEN__ diff --git a/src/main.cpp b/src/main.cpp index 8987a005..c30985be 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -46,6 +46,13 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include + +// set once the configuration is loaded and the main loop runs (see fredygames_save()) +static bool FredyGamesCanSave{false}; +#endif // __EMSCRIPTEN__ + // NOTE switch to nested namespace definition (namespace A::B::C { ... }) (since C++17) namespace viewizard { namespace astromenace { @@ -101,7 +108,12 @@ static bool VideoConfig(bool FirstStart) } if (FirstStart) { - if (!DetectFullscreenSize().empty()) { + if (!DetectFullscreenSize().empty() +#ifdef __EMSCRIPTEN__ + // in the browser the page scales the canvas to fit the player, so start in a window + && DetectWindowSizeArray().empty() +#endif // __EMSCRIPTEN__ + ) { ChangeGameConfig().Width = DetectFullscreenSize().back().Width; ChangeGameConfig().Height = DetectFullscreenSize().back().Height; ChangeGameConfig().Fullscreen = true; @@ -372,6 +384,19 @@ static void Loop() } // astromenace namespace } // viewizard namespace +#ifdef __EMSCRIPTEN__ +/* + * Called by the web page when the tab is hidden or closed: the game itself only + * saves when switching between menu and mission, and on quit. + */ +extern "C" EMSCRIPTEN_KEEPALIVE void fredygames_save() +{ + if (FredyGamesCanSave) { + viewizard::astromenace::SaveXMLConfigFile(); + } +} +#endif // __EMSCRIPTEN__ + /* * Main. */ @@ -525,7 +550,13 @@ RecreateWindow: InitMenu(eMenuStatus::MAIN_MENU); // Main loop. +#ifdef __EMSCRIPTEN__ + FredyGamesCanSave = true; +#endif // __EMSCRIPTEN__ Loop(); +#ifdef __EMSCRIPTEN__ + FredyGamesCanSave = false; +#endif // __EMSCRIPTEN__ CursorRelease(); DestroyInfoObject();