diff --git a/README.md b/README.md index 4e7eaef0..a6bd8eff 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ To achieve our goal of platform independence, we need to replace any Windows-onl | WinMM, DirectSound (Audio) | [SDL3](https://www.libsdl.org/), [miniaudio](https://miniaud.io/) | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable+%22%2F%2F+%5Blibrary%3Aaudio%5D%22&type=code) | | DirectDraw (2D video) | [SDL3](https://www.libsdl.org/) | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable+%22%2F%2F+%5Blibrary%3A2d%5D%22&type=code) | | [Smacker](https://github.com/isledecomp/isle/tree/master/3rdparty/smacker) | [libsmacker](https://github.com/foxtacles/libsmacker) | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable%20%22%2F%2F%20%5Blibrary%3Alibsmacker%5D%22&type=code) | -| Direct3D (3D video) | [SDL3 (Vulkan, Metal, D3D12)](https://www.libsdl.org/), D3D9, OpenGL, OpenGL ES, Software | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable+%22%2F%2F+%5Blibrary%3A3d%5D%22&type=code) | +| Direct3D (3D video) | [SDL3 (Vulkan, Metal, D3D12)](https://www.libsdl.org/), D3D9, OpenGL 1.1, OpenGL ES 2.0, OpenGL ES 3.0, Software | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable+%22%2F%2F+%5Blibrary%3A3d%5D%22&type=code) | | Direct3D Retained Mode | Custom re-implementation | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable+%22%2F%2F+%5Blibrary%3Aretained%5D%22&type=code) | | [SmartHeap](https://github.com/isledecomp/isle/tree/master/3rdparty/smartheap) | Default memory allocator | - | - | diff --git a/miniwin/CMakeLists.txt b/miniwin/CMakeLists.txt index 452d5144..07f40b04 100644 --- a/miniwin/CMakeLists.txt +++ b/miniwin/CMakeLists.txt @@ -44,6 +44,18 @@ if(NOT WINDOWS_STORE) message(STATUS "🧩 OpenGL 1.x support not enabled — needs OpenGL") endif() + find_library(OPENGL_ES2_LIBRARY NAMES GLESv2) + if(EMSCRIPTEN OR OPENGL_ES2_LIBRARY) + message(STATUS "Found OpenGL: enabling OpenGL ES 2.x renderer") + target_sources(miniwin PRIVATE src/d3drm/backends/opengles2/renderer.cpp) + list(APPEND GRAPHICS_BACKENDS USE_OPENGLES2) + if(OPENGL_ES2_LIBRARY) + target_link_libraries(miniwin PRIVATE ${OPENGL_ES2_LIBRARY}) + endif() + else() + message(STATUS "🧩 OpenGL ES 2.x support not enabled") + endif() + find_library(OPENGL_ES3_LIBRARY NAMES GLESv3 GLESv2) if(EMSCRIPTEN OR OPENGL_ES3_LIBRARY) message(STATUS "Found OpenGL: enabling OpenGL ES 3.x renderer") diff --git a/miniwin/src/d3drm/backends/opengles2/renderer.cpp b/miniwin/src/d3drm/backends/opengles2/renderer.cpp new file mode 100644 index 00000000..2b46190b --- /dev/null +++ b/miniwin/src/d3drm/backends/opengles2/renderer.cpp @@ -0,0 +1,898 @@ +#include "d3drmrenderer_opengles2.h" +#include "meshutils.h" + +#include +#include +#include +#include +#include + +static GLuint CompileShader(GLenum type, const char* source) +{ + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &source, nullptr); + glCompileShader(shader); + GLint success; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (!success) { + GLint logLength = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength); + if (logLength > 0) { + std::vector log(logLength); + glGetShaderInfoLog(shader, logLength, nullptr, log.data()); + SDL_Log("Shader compile error: %s", log.data()); + } + else { + SDL_Log("CompileShader (%s)", SDL_GetError()); + } + glDeleteShader(shader); + return 0; + } + return shader; +} + +struct SceneLightGLES2 { + float color[4]; + float position[4]; + float direction[4]; +}; + +Direct3DRMRenderer* OpenGLES2Renderer::Create(DWORD width, DWORD height, float anisotropic) +{ + // We have to reset the attributes here after having enumerated the + // OpenGL ES 2.0 renderer, or else SDL gets very confused by SDL_GL_DEPTH_SIZE + // call below when on an EGL-based backend, and crashes with EGL_BAD_MATCH. + SDL_GL_ResetAttributes(); + // But ResetAttributes resets it to 16. + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + 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 (!DDWindow) { + SDL_Log("No window handler"); + return nullptr; + } + + SDL_GLContext context = SDL_GL_CreateContext(DDWindow); + if (!context) { + SDL_Log("SDL_GL_CreateContext: %s", SDL_GetError()); + return nullptr; + } + + if (!SDL_GL_MakeCurrent(DDWindow, context)) { + SDL_GL_DestroyContext(context); + return nullptr; + } + + glDepthFunc(GL_LEQUAL); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CW); + + const char* vertexShaderSource = R"( + attribute vec3 a_position; + attribute vec3 a_normal; + attribute vec2 a_texCoord; + + uniform mat4 u_modelViewMatrix; + uniform mat3 u_normalMatrix; + uniform mat4 u_projectionMatrix; + + varying vec3 v_viewPos; + varying vec3 v_normal; + varying vec2 v_texCoord; + + void main() { + vec4 viewPos = u_modelViewMatrix * vec4(a_position, 1.0); + gl_Position = u_projectionMatrix * viewPos; + v_viewPos = viewPos.xyz; + v_normal = normalize(u_normalMatrix * a_normal); + v_texCoord = a_texCoord; + } + )"; + + const char* fragmentShaderSource = R"( + precision mediump float; + + struct SceneLight { + vec4 color; + vec4 position; + vec4 direction; + }; + + uniform SceneLight u_lights[3]; + uniform int u_lightCount; + + varying vec3 v_viewPos; + varying vec3 v_normal; + varying vec2 v_texCoord; + + uniform float u_shininess; + uniform vec4 u_color; + uniform int u_useTexture; + uniform sampler2D u_texture; + + void main() { + vec3 diffuse = vec3(0.0); + vec3 specular = vec3(0.0); + + for (int i = 0; i < 3; ++i) { + if (i >= u_lightCount) break; + + vec3 lightColor = u_lights[i].color.rgb; + + if (u_lights[i].position.w == 0.0 && u_lights[i].direction.w == 0.0) { + diffuse += lightColor; + continue; + } + + vec3 lightVec; + if (u_lights[i].direction.w == 1.0) { + lightVec = -normalize(u_lights[i].direction.xyz); + } + else { + lightVec = u_lights[i].position.xyz - v_viewPos; + } + lightVec = normalize(lightVec); + + float dotNL = max(dot(v_normal, lightVec), 0.0); + if (dotNL > 0.0) { + // Diffuse contribution + diffuse += dotNL * lightColor; + + // Specular + if (u_shininess > 0.0 && u_lights[i].direction.w == 1.0) { + vec3 viewVec = normalize(-v_viewPos); + vec3 H = normalize(lightVec + viewVec); + float dotNH = max(dot(v_normal, H), 0.0); + float spec = pow(dotNH, u_shininess); + specular += spec * lightColor; + } + } + } + + vec4 finalColor = u_color; + finalColor.rgb = clamp(diffuse * u_color.rgb + specular, 0.0, 1.0); + if (u_useTexture != 0) { + vec4 texel = texture2D(u_texture, v_texCoord); + finalColor.rgb = clamp(texel.rgb * finalColor.rgb, 0.0, 1.0); + finalColor.a = texel.a; + } + + gl_FragColor = finalColor; + } + )"; + + GLuint vs = CompileShader(GL_VERTEX_SHADER, vertexShaderSource); + GLuint fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); + + GLuint shaderProgram = glCreateProgram(); + glAttachShader(shaderProgram, vs); + glAttachShader(shaderProgram, fs); + glBindAttribLocation(shaderProgram, 0, "a_position"); + glBindAttribLocation(shaderProgram, 1, "a_normal"); + glBindAttribLocation(shaderProgram, 2, "a_texCoord"); + glLinkProgram(shaderProgram); + glDeleteShader(vs); + glDeleteShader(fs); + + return new OpenGLES2Renderer(width, height, anisotropic, context, shaderProgram); +} + +GLES2MeshCacheEntry GLES2UploadMesh(const MeshGroup& meshGroup, bool forceUV = false) +{ + GLES2MeshCacheEntry cache{&meshGroup, meshGroup.version}; + + cache.flat = meshGroup.quality == D3DRMRENDER_FLAT || meshGroup.quality == D3DRMRENDER_UNLITFLAT; + + std::vector vertices; + if (cache.flat) { + FlattenSurfaces( + meshGroup.vertices.data(), + meshGroup.vertices.size(), + meshGroup.indices.data(), + meshGroup.indices.size(), + meshGroup.texture != nullptr || forceUV, + vertices, + cache.indices + ); + } + else { + vertices = meshGroup.vertices; + cache.indices.resize(meshGroup.indices.size()); + std::transform(meshGroup.indices.begin(), meshGroup.indices.end(), cache.indices.begin(), [](DWORD index) { + return static_cast(index); + }); + } + + std::vector texcoords; + if (meshGroup.texture || forceUV) { + texcoords.resize(vertices.size()); + std::transform(vertices.begin(), vertices.end(), texcoords.begin(), [](const D3DRMVERTEX& v) { + return v.texCoord; + }); + } + std::vector positions(vertices.size()); + std::transform(vertices.begin(), vertices.end(), positions.begin(), [](const D3DRMVERTEX& v) { + return v.position; + }); + std::vector normals(vertices.size()); + std::transform(vertices.begin(), vertices.end(), normals.begin(), [](const D3DRMVERTEX& v) { return v.normal; }); + + glGenBuffers(1, &cache.vboPositions); + glBindBuffer(GL_ARRAY_BUFFER, cache.vboPositions); + glBufferData(GL_ARRAY_BUFFER, positions.size() * sizeof(D3DVECTOR), positions.data(), GL_STATIC_DRAW); + + glGenBuffers(1, &cache.vboNormals); + glBindBuffer(GL_ARRAY_BUFFER, cache.vboNormals); + glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(D3DVECTOR), normals.data(), GL_STATIC_DRAW); + + if (meshGroup.texture || forceUV) { + glGenBuffers(1, &cache.vboTexcoords); + glBindBuffer(GL_ARRAY_BUFFER, cache.vboTexcoords); + glBufferData(GL_ARRAY_BUFFER, texcoords.size() * sizeof(TexCoord), texcoords.data(), GL_STATIC_DRAW); + } + + glGenBuffers(1, &cache.ibo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cache.ibo); + glBufferData( + GL_ELEMENT_ARRAY_BUFFER, + cache.indices.size() * sizeof(cache.indices[0]), + cache.indices.data(), + GL_STATIC_DRAW + ); + + return cache; +} + +bool OpenGLES2Renderer::UploadTexture(SDL_Surface* source, GLuint& outTexId, bool isUI) +{ + SDL_Surface* surf = source; + if (source->format != SDL_PIXELFORMAT_RGBA32) { + surf = SDL_ConvertSurface(source, SDL_PIXELFORMAT_RGBA32); + if (!surf) { + return false; + } + } + + glGenTextures(1, &outTexId); + glBindTexture(GL_TEXTURE_2D, outTexId); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surf->w, surf->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, surf->pixels); + + if (isUI) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + else { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + if (m_anisotropic > 1.0f) { + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, m_anisotropic); + } + glGenerateMipmap(GL_TEXTURE_2D); + } + + if (surf != source) { + SDL_DestroySurface(surf); + } + + return true; +} + +OpenGLES2Renderer::OpenGLES2Renderer( + DWORD width, + DWORD height, + float anisotropic, + SDL_GLContext context, + GLuint shaderProgram +) + : m_context(context), m_shaderProgram(shaderProgram), m_anisotropic(anisotropic) +{ + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + bool anisoAvailable = SDL_GL_ExtensionSupported("GL_EXT_texture_filter_anisotropic"); + GLfloat maxAniso = 0.0f; + if (anisoAvailable) { + glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxAniso); + } + if (m_anisotropic > maxAniso) { + m_anisotropic = maxAniso; + } + SDL_Log( + "Anisotropic is %s. Requested: %f, active: %f, max aniso: %f", + m_anisotropic > 1.0f ? "on" : "off", + anisotropic, + m_anisotropic, + maxAniso + ); + + m_virtualWidth = width; + m_virtualHeight = height; + ViewportTransform viewportTransform = {1.0f, 0.0f, 0.0f}; + Resize(width, height, viewportTransform); + + SDL_Surface* dummySurface = SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32); + if (!dummySurface) { + SDL_Log("Failed to create surface: %s", SDL_GetError()); + return; + } + if (!SDL_LockSurface(dummySurface)) { + SDL_Log("Failed to lock surface: %s", SDL_GetError()); + SDL_DestroySurface(dummySurface); + return; + } + ((Uint32*) dummySurface->pixels)[0] = 0xFFFFFFFF; + SDL_UnlockSurface(dummySurface); + + UploadTexture(dummySurface, m_dummyTexture, false); + if (!m_dummyTexture) { + SDL_DestroySurface(dummySurface); + SDL_Log("Failed to create surface: %s", SDL_GetError()); + return; + } + SDL_DestroySurface(dummySurface); + + m_posLoc = glGetAttribLocation(m_shaderProgram, "a_position"); + m_normLoc = glGetAttribLocation(m_shaderProgram, "a_normal"); + m_texLoc = glGetAttribLocation(m_shaderProgram, "a_texCoord"); + m_colorLoc = glGetUniformLocation(m_shaderProgram, "u_color"); + m_shinLoc = glGetUniformLocation(m_shaderProgram, "u_shininess"); + m_lightCountLoc = glGetUniformLocation(m_shaderProgram, "u_lightCount"); + m_useTextureLoc = glGetUniformLocation(m_shaderProgram, "u_useTexture"); + m_textureLoc = glGetUniformLocation(m_shaderProgram, "u_texture"); + for (int i = 0; i < 3; ++i) { + std::string base = "u_lights[" + std::to_string(i) + "]"; + u_lightLocs[i][0] = glGetUniformLocation(m_shaderProgram, (base + ".color").c_str()); + u_lightLocs[i][1] = glGetUniformLocation(m_shaderProgram, (base + ".position").c_str()); + u_lightLocs[i][2] = glGetUniformLocation(m_shaderProgram, (base + ".direction").c_str()); + } + m_modelViewMatrixLoc = glGetUniformLocation(m_shaderProgram, "u_modelViewMatrix"); + m_normalMatrixLoc = glGetUniformLocation(m_shaderProgram, "u_normalMatrix"); + m_projectionMatrixLoc = glGetUniformLocation(m_shaderProgram, "u_projectionMatrix"); + + m_uiMesh.vertices = { + {{0.0f, 0.0f, 0.0f}, {0, 0, -1}, {0.0f, 0.0f}}, + {{1.0f, 0.0f, 0.0f}, {0, 0, -1}, {1.0f, 0.0f}}, + {{1.0f, 1.0f, 0.0f}, {0, 0, -1}, {1.0f, 1.0f}}, + {{0.0f, 1.0f, 0.0f}, {0, 0, -1}, {0.0f, 1.0f}} + }; + m_uiMesh.indices = {0, 1, 2, 0, 2, 3}; + m_uiMeshCache = GLES2UploadMesh(m_uiMesh, true); + + glUseProgram(m_shaderProgram); +} + +OpenGLES2Renderer::~OpenGLES2Renderer() +{ + SDL_DestroySurface(m_renderedImage); + glDeleteTextures(1, &m_dummyTexture); + glDeleteProgram(m_shaderProgram); + glDeleteTextures(1, &m_colorTarget); + glDeleteRenderbuffers(1, &m_depthTarget); + glDeleteFramebuffers(1, &m_fbo); + SDL_GL_DestroyContext(m_context); +} + +void OpenGLES2Renderer::PushLights(const SceneLight* lightsArray, size_t count) +{ + if (count > 3) { + SDL_Log("Unsupported number of lights (%d)", static_cast(count)); + count = 3; + } + + m_lights.assign(lightsArray, lightsArray + count); +} + +void OpenGLES2Renderer::SetFrustumPlanes(const Plane* frustumPlanes) +{ +} + +void OpenGLES2Renderer::SetProjection(const D3DRMMATRIX4D& projection, D3DVALUE front, D3DVALUE back) +{ + memcpy(&m_projection, projection, sizeof(D3DRMMATRIX4D)); +} + +struct TextureDestroyContextGLS2 { + OpenGLES2Renderer* renderer; + Uint32 textureId; +}; + +void OpenGLES2Renderer::AddTextureDestroyCallback(Uint32 id, IDirect3DRMTexture* texture) +{ + auto* ctx = new TextureDestroyContextGLS2{this, id}; + texture->AddDestroyCallback( + [](IDirect3DRMObject* obj, void* arg) { + auto* ctx = static_cast(arg); + auto& cache = ctx->renderer->m_textures[ctx->textureId]; + if (cache.glTextureId != 0) { + glDeleteTextures(1, &cache.glTextureId); + cache.glTextureId = 0; + cache.texture = nullptr; + } + delete ctx; + }, + ctx + ); +} + +Uint32 OpenGLES2Renderer::GetTextureId(IDirect3DRMTexture* iTexture, bool isUI, float scaleX, float scaleY) +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + auto texture = static_cast(iTexture); + auto surface = static_cast(texture->m_surface); + + for (Uint32 i = 0; i < m_textures.size(); ++i) { + auto& tex = m_textures[i]; + if (tex.texture == texture) { + if (tex.version != texture->m_version) { + glDeleteTextures(1, &tex.glTextureId); + if (UploadTexture(surface->m_surface, tex.glTextureId, isUI)) { + tex.version = texture->m_version; + } + } + return i; + } + } + + GLuint texId; + if (!UploadTexture(surface->m_surface, texId, isUI)) { + return NO_TEXTURE_ID; + } + + for (Uint32 i = 0; i < m_textures.size(); ++i) { + auto& tex = m_textures[i]; + if (!tex.texture) { + tex.texture = texture; + tex.version = texture->m_version; + tex.glTextureId = texId; + tex.width = surface->m_surface->w; + tex.height = surface->m_surface->h; + AddTextureDestroyCallback(i, texture); + return i; + } + } + + m_textures.push_back( + {texture, texture->m_version, texId, (uint16_t) surface->m_surface->w, (uint16_t) surface->m_surface->h} + ); + AddTextureDestroyCallback((Uint32) (m_textures.size() - 1), texture); + return (Uint32) (m_textures.size() - 1); +} + +struct GLES2MeshDestroyContext { + OpenGLES2Renderer* renderer; + Uint32 id; +}; + +void OpenGLES2Renderer::AddMeshDestroyCallback(Uint32 id, IDirect3DRMMesh* mesh) +{ + auto* ctx = new GLES2MeshDestroyContext{this, id}; + mesh->AddDestroyCallback( + [](IDirect3DRMObject*, void* arg) { + auto* ctx = static_cast(arg); + auto& cache = ctx->renderer->m_meshs[ctx->id]; + cache.meshGroup = nullptr; + glDeleteBuffers(1, &cache.vboPositions); + glDeleteBuffers(1, &cache.vboNormals); + glDeleteBuffers(1, &cache.vboTexcoords); + glDeleteBuffers(1, &cache.ibo); + delete ctx; + }, + ctx + ); +} + +Uint32 OpenGLES2Renderer::GetMeshId(IDirect3DRMMesh* mesh, const MeshGroup* meshGroup) +{ + for (Uint32 i = 0; i < m_meshs.size(); ++i) { + auto& cache = m_meshs[i]; + if (cache.meshGroup == meshGroup) { + if (cache.version != meshGroup->version) { + cache = std::move(GLES2UploadMesh(*meshGroup)); + } + return i; + } + } + + auto newCache = GLES2UploadMesh(*meshGroup); + + for (Uint32 i = 0; i < m_meshs.size(); ++i) { + auto& cache = m_meshs[i]; + if (!cache.meshGroup) { + cache = std::move(newCache); + AddMeshDestroyCallback(i, mesh); + return i; + } + } + + m_meshs.push_back(std::move(newCache)); + AddMeshDestroyCallback((Uint32) (m_meshs.size() - 1), mesh); + return (Uint32) (m_meshs.size() - 1); +} + +HRESULT OpenGLES2Renderer::BeginFrame() +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + m_dirty = true; + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + glEnable(GL_CULL_FACE); + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + + SceneLightGLES2 lightData[3]; + int lightCount = std::min(static_cast(m_lights.size()), 3); + + for (int i = 0; i < lightCount; ++i) { + const auto& src = m_lights[i]; + lightData[i].color[0] = src.color.r; + lightData[i].color[1] = src.color.g; + lightData[i].color[2] = src.color.b; + lightData[i].color[3] = src.color.a; + + lightData[i].position[0] = src.position.x; + lightData[i].position[1] = src.position.y; + lightData[i].position[2] = src.position.z; + lightData[i].position[3] = src.positional; + + lightData[i].direction[0] = src.direction.x; + lightData[i].direction[1] = src.direction.y; + lightData[i].direction[2] = src.direction.z; + lightData[i].direction[3] = src.directional; + } + + for (int i = 0; i < lightCount; ++i) { + glUniform4fv(u_lightLocs[i][0], 1, lightData[i].color); + glUniform4fv(u_lightLocs[i][1], 1, lightData[i].position); + glUniform4fv(u_lightLocs[i][2], 1, lightData[i].direction); + } + glUniform1i(m_lightCountLoc, lightCount); + return DD_OK; +} + +void OpenGLES2Renderer::EnableTransparency() +{ + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); +} + +void OpenGLES2Renderer::SubmitDraw( + DWORD meshId, + const D3DRMMATRIX4D& modelViewMatrix, + const D3DRMMATRIX4D& worldMatrix, + const D3DRMMATRIX4D& viewMatrix, + const Matrix3x3& normalMatrix, + const Appearance& appearance +) +{ + auto& mesh = m_meshs[meshId]; + + glUniformMatrix4fv(m_modelViewMatrixLoc, 1, GL_FALSE, &modelViewMatrix[0][0]); + glUniformMatrix3fv(m_normalMatrixLoc, 1, GL_FALSE, &normalMatrix[0][0]); + glUniformMatrix4fv(m_projectionMatrixLoc, 1, GL_FALSE, &m_projection[0][0]); + + glUniform4f( + m_colorLoc, + appearance.color.r / 255.0f, + appearance.color.g / 255.0f, + appearance.color.b / 255.0f, + appearance.color.a / 255.0f + ); + + glUniform1f(m_shinLoc, appearance.shininess); + + if (appearance.textureId != NO_TEXTURE_ID) { + glUniform1i(m_useTextureLoc, 1); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_textures[appearance.textureId].glTextureId); + glUniform1i(m_textureLoc, 0); + } + else { + glUniform1i(m_useTextureLoc, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_dummyTexture); + glUniform1i(m_textureLoc, 0); + } + + glBindBuffer(GL_ARRAY_BUFFER, mesh.vboPositions); + glEnableVertexAttribArray(m_posLoc); + glVertexAttribPointer(m_posLoc, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + glBindBuffer(GL_ARRAY_BUFFER, mesh.vboNormals); + glEnableVertexAttribArray(m_normLoc); + glVertexAttribPointer(m_normLoc, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + if (appearance.textureId != NO_TEXTURE_ID) { + glBindBuffer(GL_ARRAY_BUFFER, mesh.vboTexcoords); + glEnableVertexAttribArray(m_texLoc); + glVertexAttribPointer(m_texLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + } + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ibo); + glDrawElements(GL_TRIANGLES, static_cast(mesh.indices.size()), GL_UNSIGNED_SHORT, nullptr); + + glDisableVertexAttribArray(m_normLoc); + glDisableVertexAttribArray(m_texLoc); +} + +HRESULT OpenGLES2Renderer::FinalizeFrame() +{ + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + return DD_OK; +} + +void OpenGLES2Renderer::Resize(int width, int height, const ViewportTransform& viewportTransform) +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + m_width = width; + m_height = height; + m_viewportTransform = viewportTransform; + if (m_renderedImage) { + SDL_DestroySurface(m_renderedImage); + } + m_renderedImage = SDL_CreateSurface(m_width, m_height, SDL_PIXELFORMAT_RGBA32); + + if (m_colorTarget) { + glDeleteTextures(1, &m_colorTarget); + m_colorTarget = 0; + } + if (m_depthTarget) { + glDeleteRenderbuffers(1, &m_depthTarget); + m_depthTarget = 0; + } + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + // Create color texture + glGenTextures(1, &m_colorTarget); + glBindTexture(GL_TEXTURE_2D, m_colorTarget); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorTarget, 0); + + // Create depth renderbuffer + glGenRenderbuffers(1, &m_depthTarget); + glBindRenderbuffer(GL_RENDERBUFFER, m_depthTarget); + + if (SDL_GL_ExtensionSupported("GL_OES_depth24")) { + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24_OES, width, height); + } + else if (SDL_GL_ExtensionSupported("GL_OES_depth32")) { + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32_OES, width, height); + } + else { + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height); + } + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depthTarget); + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + SDL_Log("FBO incomplete: 0x%X", status); + } + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + glViewport(0, 0, m_width, m_height); +} + +void OpenGLES2Renderer::Clear(float r, float g, float b) +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + m_dirty = true; + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + glClearColor(r, g, b, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +} + +void OpenGLES2Renderer::Flip() +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + if (!m_dirty) { + return; + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + glDisable(GL_DEPTH_TEST); + glFrontFace(GL_CCW); + glDepthMask(GL_FALSE); + + glUniform4f(m_colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); + glUniform1f(m_shinLoc, 0.0f); + + float ambient[] = {1.0f, 1.0f, 1.0f, 1.0f}; + float blank[] = {0.0f, 0.0f, 0.0f, 0.0f}; + glUniform4fv(u_lightLocs[0][0], 1, ambient); + glUniform4fv(u_lightLocs[0][1], 1, blank); + glUniform4fv(u_lightLocs[0][2], 1, blank); + glUniform1i(m_lightCountLoc, 1); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_colorTarget); + glUniform1i(m_textureLoc, 0); + glUniform1i(m_useTextureLoc, 1); + + D3DRMMATRIX4D projection; + D3DRMMATRIX4D modelViewMatrix = { + {(float) m_width, 0.0f, 0.0f, 0.0f}, + {0.0f, (float) -m_height, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + {0.0f, (float) m_height, 0.0f, 1.0f} + }; + glUniformMatrix4fv(m_modelViewMatrixLoc, 1, GL_FALSE, &modelViewMatrix[0][0]); + Matrix3x3 identity = {{1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}}; + glUniformMatrix3fv(m_normalMatrixLoc, 1, GL_FALSE, &identity[0][0]); + CreateOrthographicProjection((float) m_width, (float) m_height, projection); + glUniformMatrix4fv(m_projectionMatrixLoc, 1, GL_FALSE, &projection[0][0]); + + glDisable(GL_SCISSOR_TEST); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glBindBuffer(GL_ARRAY_BUFFER, m_uiMeshCache.vboPositions); + glEnableVertexAttribArray(m_posLoc); + glVertexAttribPointer(m_posLoc, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + glBindBuffer(GL_ARRAY_BUFFER, m_uiMeshCache.vboTexcoords); + glEnableVertexAttribArray(m_texLoc); + glVertexAttribPointer(m_texLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_uiMeshCache.ibo); + glDrawElements(GL_TRIANGLES, static_cast(m_uiMeshCache.indices.size()), GL_UNSIGNED_SHORT, nullptr); + + glDisableVertexAttribArray(m_texLoc); + + SDL_GL_SwapWindow(DDWindow); + glFrontFace(GL_CW); + m_dirty = false; +} + +void OpenGLES2Renderer::Draw2DImage(Uint32 textureId, const SDL_Rect& srcRect, const SDL_Rect& dstRect, FColor color) +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + m_dirty = true; + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + + float ambient[] = {1.0f, 1.0f, 1.0f, 1.0f}; + float blank[] = {0.0f, 0.0f, 0.0f, 0.0f}; + glUniform4fv(u_lightLocs[0][0], 1, ambient); + glUniform4fv(u_lightLocs[0][1], 1, blank); + glUniform4fv(u_lightLocs[0][2], 1, blank); + glUniform1i(m_lightCountLoc, 1); + + glUniform4f(m_colorLoc, color.r, color.g, color.b, color.a); + glUniform1f(m_shinLoc, 0.0f); + + SDL_Rect expandedDstRect; + if (textureId != NO_TEXTURE_ID) { + const GLES2TextureCacheEntry& texture = m_textures[textureId]; + float scaleX = static_cast(dstRect.w) / srcRect.w; + float scaleY = static_cast(dstRect.h) / srcRect.h; + expandedDstRect = { + static_cast(std::round(dstRect.x - srcRect.x * scaleX)), + static_cast(std::round(dstRect.y - srcRect.y * scaleY)), + static_cast(std::round(texture.width * scaleX)), + static_cast(std::round(texture.height * scaleY)) + }; + + glUniform1i(m_useTextureLoc, 1); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture.glTextureId); + glUniform1i(m_textureLoc, 0); + } + else { + expandedDstRect = dstRect; + glUniform1i(m_useTextureLoc, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_dummyTexture); + glUniform1i(m_textureLoc, 0); + } + + D3DRMMATRIX4D modelView, projection; + Create2DTransformMatrix( + expandedDstRect, + m_viewportTransform.scale, + m_viewportTransform.offsetX, + m_viewportTransform.offsetY, + modelView + ); + + glUniformMatrix4fv(m_modelViewMatrixLoc, 1, GL_FALSE, &modelView[0][0]); + Matrix3x3 identity = {{1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}}; + glUniformMatrix3fv(m_normalMatrixLoc, 1, GL_FALSE, &identity[0][0]); + CreateOrthographicProjection((float) m_width, (float) m_height, projection); + glUniformMatrix4fv(m_projectionMatrixLoc, 1, GL_FALSE, &projection[0][0]); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glEnable(GL_SCISSOR_TEST); + glScissor( + static_cast(std::round(dstRect.x * m_viewportTransform.scale + m_viewportTransform.offsetX)), + m_height - static_cast( + std::round((dstRect.y + dstRect.h) * m_viewportTransform.scale + m_viewportTransform.offsetY) + ), + static_cast(std::round(dstRect.w * m_viewportTransform.scale)), + static_cast(std::round(dstRect.h * m_viewportTransform.scale)) + ); + + glBindBuffer(GL_ARRAY_BUFFER, m_uiMeshCache.vboPositions); + glEnableVertexAttribArray(m_posLoc); + glVertexAttribPointer(m_posLoc, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + glBindBuffer(GL_ARRAY_BUFFER, m_uiMeshCache.vboTexcoords); + glEnableVertexAttribArray(m_texLoc); + glVertexAttribPointer(m_texLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_uiMeshCache.ibo); + glDrawElements(GL_TRIANGLES, static_cast(m_uiMeshCache.indices.size()), GL_UNSIGNED_SHORT, nullptr); + + glDisableVertexAttribArray(m_texLoc); + glDisable(GL_SCISSOR_TEST); +} + +void OpenGLES2Renderer::Download(SDL_Surface* target) +{ + glFinish(); + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glReadPixels(0, 0, m_width, m_height, GL_RGBA, GL_UNSIGNED_BYTE, m_renderedImage->pixels); + + SDL_Rect srcRect = { + static_cast(m_viewportTransform.offsetX), + static_cast(m_viewportTransform.offsetY), + static_cast(target->w * m_viewportTransform.scale), + static_cast(target->h * m_viewportTransform.scale), + }; + + SDL_Surface* bufferClone = SDL_CreateSurface(target->w, target->h, SDL_PIXELFORMAT_RGBA32); + if (!bufferClone) { + SDL_Log("SDL_CreateSurface: %s", SDL_GetError()); + return; + } + + SDL_BlitSurfaceScaled(m_renderedImage, &srcRect, bufferClone, nullptr, SDL_SCALEMODE_NEAREST); + + // Flip image vertically into target + SDL_Rect rowSrc = {0, 0, bufferClone->w, 1}; + SDL_Rect rowDst = {0, 0, bufferClone->w, 1}; + for (int y = 0; y < bufferClone->h; ++y) { + rowSrc.y = y; + rowDst.y = bufferClone->h - 1 - y; + SDL_BlitSurface(bufferClone, &rowSrc, target, &rowDst); + } + + SDL_DestroySurface(bufferClone); +} + +void OpenGLES2Renderer::SetDither(bool dither) +{ + if (dither) { + glEnable(GL_DITHER); + } + else { + glDisable(GL_DITHER); + } +} diff --git a/miniwin/src/d3drm/d3drmrenderer.cpp b/miniwin/src/d3drm/d3drmrenderer.cpp index 8fe210b5..f1e1fb8b 100644 --- a/miniwin/src/d3drm/d3drmrenderer.cpp +++ b/miniwin/src/d3drm/d3drmrenderer.cpp @@ -2,6 +2,9 @@ #ifdef USE_OPENGL1 #include "d3drmrenderer_opengl1.h" #endif +#ifdef USE_OPENGLES2 +#include "d3drmrenderer_opengles2.h" +#endif #ifdef USE_OPENGLES3 #include "d3drmrenderer_opengles3.h" #endif @@ -44,6 +47,11 @@ Direct3DRMRenderer* CreateDirect3DRMRenderer( ); } #endif +#ifdef USE_OPENGLES2 + if (SDL_memcmp(guid, &OpenGLES2_GUID, sizeof(GUID)) == 0) { + return OpenGLES2Renderer::Create(DDSDesc.dwWidth, DDSDesc.dwHeight, d3d->GetAnisotropic()); + } +#endif #ifdef USE_OPENGL1 if (SDL_memcmp(guid, &OpenGL1_GUID, sizeof(GUID)) == 0) { return OpenGL1Renderer::Create(DDSDesc.dwWidth, DDSDesc.dwHeight, d3d->GetMSAASamples()); @@ -70,6 +78,9 @@ void Direct3DRMRenderer_EnumDevices(const IDirect3DMiniwin* d3d, LPD3DENUMDEVICE #ifdef USE_OPENGLES3 OpenGLES3Renderer_EnumDevice(d3d, cb, ctx); #endif +#ifdef USE_OPENGLES2 + OpenGLES2Renderer_EnumDevice(d3d, cb, ctx); +#endif #ifdef USE_OPENGL1 OpenGL1Renderer_EnumDevice(d3d, cb, ctx); #endif diff --git a/miniwin/src/internal/d3drmrenderer_opengles2.h b/miniwin/src/internal/d3drmrenderer_opengles2.h new file mode 100644 index 00000000..6a95d73f --- /dev/null +++ b/miniwin/src/internal/d3drmrenderer_opengles2.h @@ -0,0 +1,128 @@ +#pragma once + +#include "d3drmrenderer.h" +#include "d3drmtexture_impl.h" +#include "ddraw_impl.h" + +#include +#include +#include + +DEFINE_GUID(OpenGLES2_GUID, 0x682656F3, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06); + +struct GLES2TextureCacheEntry { + IDirect3DRMTexture* texture; + Uint32 version; + GLuint glTextureId; + uint16_t width; + uint16_t height; +}; + +struct GLES2MeshCacheEntry { + const MeshGroup* meshGroup; + int version; + bool flat; + + std::vector indices; + GLuint vboPositions; + GLuint vboNormals; + GLuint vboTexcoords; + GLuint ibo; +}; + +class OpenGLES2Renderer : public Direct3DRMRenderer { +public: + static Direct3DRMRenderer* Create(DWORD width, DWORD height, float anisotropic); + OpenGLES2Renderer(DWORD width, DWORD height, float anisotropic, SDL_GLContext context, GLuint shaderProgram); + ~OpenGLES2Renderer() override; + + void PushLights(const SceneLight* lightsArray, size_t count) override; + void SetProjection(const D3DRMMATRIX4D& projection, D3DVALUE front, D3DVALUE back) override; + void SetFrustumPlanes(const Plane* frustumPlanes) override; + Uint32 GetTextureId(IDirect3DRMTexture* texture, bool isUI, float scaleX, float scaleY) override; + Uint32 GetMeshId(IDirect3DRMMesh* mesh, const MeshGroup* meshGroup) override; + HRESULT BeginFrame() override; + void EnableTransparency() override; + void SubmitDraw( + DWORD meshId, + const D3DRMMATRIX4D& modelViewMatrix, + const D3DRMMATRIX4D& worldMatrix, + const D3DRMMATRIX4D& viewMatrix, + const Matrix3x3& normalMatrix, + const Appearance& appearance + ) override; + HRESULT FinalizeFrame() override; + void Resize(int width, int height, const ViewportTransform& viewportTransform) override; + void Clear(float r, float g, float b) override; + void Flip() override; + void Draw2DImage(Uint32 textureId, const SDL_Rect& srcRect, const SDL_Rect& dstRect, FColor color) override; + void Download(SDL_Surface* target) override; + void SetDither(bool dither) override; + +private: + void AddTextureDestroyCallback(Uint32 id, IDirect3DRMTexture* texture); + void AddMeshDestroyCallback(Uint32 id, IDirect3DRMMesh* mesh); + bool UploadTexture(SDL_Surface* source, GLuint& outTexId, bool isUI); + + MeshGroup m_uiMesh; + GLES2MeshCacheEntry m_uiMeshCache; + std::vector m_textures; + std::vector m_meshs; + D3DRMMATRIX4D m_projection; + SDL_Surface* m_renderedImage = nullptr; + bool m_dirty = false; + std::vector m_lights; + SDL_GLContext m_context; + float m_anisotropic; + GLuint m_fbo; + GLuint m_colorTarget; + GLuint m_depthTarget; + GLuint m_shaderProgram; + GLuint m_dummyTexture; + GLint m_posLoc; + GLint m_normLoc; + GLint m_texLoc; + GLint m_colorLoc; + GLint m_shinLoc; + GLint m_lightCountLoc; + GLint m_useTextureLoc; + GLint m_textureLoc; + GLint u_lightLocs[3][3]; + GLint m_modelViewMatrixLoc; + GLint m_normalMatrixLoc; + GLint m_projectionMatrixLoc; + ViewportTransform m_viewportTransform; +}; + +inline static void OpenGLES2Renderer_EnumDevice(const IDirect3DMiniwin* d3d, LPD3DENUMDEVICESCALLBACK cb, void* ctx) +{ + Direct3DRMRenderer* device = OpenGLES2Renderer::Create(640, 480, d3d->GetAnisotropic()); + if (!device) { + return; + } + + D3DDEVICEDESC halDesc = {}; + halDesc.dcmColorModel = D3DCOLOR_RGB; + halDesc.dwFlags = D3DDD_DEVICEZBUFFERBITDEPTH; + halDesc.dwDeviceZBufferBitDepth = DDBD_16; + halDesc.dwDeviceRenderBitDepth = DDBD_32; + halDesc.dpcTriCaps.dwTextureCaps = D3DPTEXTURECAPS_PERSPECTIVE; + halDesc.dpcTriCaps.dwShadeCaps = D3DPSHADECAPS_ALPHAFLATBLEND; + halDesc.dpcTriCaps.dwTextureFilterCaps = D3DPTFILTERCAPS_LINEAR; + + const char* extensions = (const char*) glGetString(GL_EXTENSIONS); + if (extensions) { + if (strstr(extensions, "GL_OES_depth24")) { + halDesc.dwDeviceZBufferBitDepth |= DDBD_24; + } + if (strstr(extensions, "GL_OES_depth32")) { + halDesc.dwDeviceZBufferBitDepth |= DDBD_32; + } + } + + delete device; + + D3DDEVICEDESC helDesc = {}; + + EnumDevice(cb, ctx, "OpenGL ES 2.0 HAL", &halDesc, &helDesc, OpenGLES2_GUID); +}