diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt index 1c570e1..c63869e 100644 --- a/android/app/src/main/cpp/CMakeLists.txt +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -112,6 +112,8 @@ add_library(super3 SHARED android_shim.cpp model3_stubs.cpp android_input_system.cpp + gles_presenter.cpp + gles_stub_render3d.cpp render2d_android.cpp ${SUPER3_SOURCES} ${M68K_GENERATED_SOURCES} @@ -129,10 +131,14 @@ target_include_directories(super3 PRIVATE find_library(log-lib log) find_library(android-lib android) find_library(z-lib z) +find_library(egl-lib EGL) +find_library(glesv3-lib GLESv3) target_link_libraries(super3 PRIVATE ${log-lib} ${android-lib} ${z-lib} + ${egl-lib} + ${glesv3-lib} SDL2::SDL2 # present when SUPER3_FETCH_SDL2 is ON ) diff --git a/android/app/src/main/cpp/gles_presenter.cpp b/android/app/src/main/cpp/gles_presenter.cpp new file mode 100644 index 0000000..25acb51 --- /dev/null +++ b/android/app/src/main/cpp/gles_presenter.cpp @@ -0,0 +1,230 @@ +#include "gles_presenter.h" + +#include + +#include +#include + +namespace { +static GLuint Compile(GLenum type, const char* src) +{ + GLuint sh = glCreateShader(type); + glShaderSource(sh, 1, &src, nullptr); + glCompileShader(sh); + GLint ok = 0; + glGetShaderiv(sh, GL_COMPILE_STATUS, &ok); + if (!ok) + { + char log[2048]; + GLsizei n = 0; + glGetShaderInfoLog(sh, (GLsizei)sizeof(log), &n, log); + glDeleteShader(sh); + return 0; + } + return sh; +} +} // namespace + +bool GlesPresenter::Init() +{ + if (!CreateProgram()) + return false; + if (!CreateGeometry()) + return false; + + glGenTextures(1, &m_tex); + glBindTexture(GL_TEXTURE_2D, m_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + + return true; +} + +void GlesPresenter::Shutdown() +{ + if (m_tex) { glDeleteTextures(1, &m_tex); m_tex = 0; } + if (m_vbo) { glDeleteBuffers(1, &m_vbo); m_vbo = 0; } + if (m_vao) { glDeleteVertexArrays(1, &m_vao); m_vao = 0; } + if (m_program) { glDeleteProgram(m_program); m_program = 0; } + m_uTex = -1; + m_uploadRGBA.clear(); + m_outputW = m_outputH = m_srcW = m_srcH = 0; +} + +bool GlesPresenter::CreateProgram() +{ + static const char* vs = R"glsl( + #version 300 es + layout(location=0) in vec2 aPos; + layout(location=1) in vec2 aUv; + out vec2 vUv; + void main() { + vUv = aUv; + gl_Position = vec4(aPos, 0.0, 1.0); + } + )glsl"; + + static const char* fs = R"glsl( + #version 300 es + precision mediump float; + in vec2 vUv; + uniform sampler2D uTex; + out vec4 oColor; + void main() { + oColor = texture(uTex, vUv); + } + )glsl"; + + GLuint v = Compile(GL_VERTEX_SHADER, vs); + GLuint f = Compile(GL_FRAGMENT_SHADER, fs); + if (!v || !f) + { + if (v) glDeleteShader(v); + if (f) glDeleteShader(f); + return false; + } + + m_program = glCreateProgram(); + glAttachShader(m_program, v); + glAttachShader(m_program, f); + glLinkProgram(m_program); + glDeleteShader(v); + glDeleteShader(f); + + GLint ok = 0; + glGetProgramiv(m_program, GL_LINK_STATUS, &ok); + if (!ok) + { + glDeleteProgram(m_program); + m_program = 0; + return false; + } + + m_uTex = glGetUniformLocation(m_program, "uTex"); + return true; +} + +bool GlesPresenter::CreateGeometry() +{ + glGenVertexArrays(1, &m_vao); + glGenBuffers(1, &m_vbo); + glBindVertexArray(m_vao); + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(m_verts), m_verts, GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4, (void*)0); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4, (void*)(sizeof(float) * 2)); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + return true; +} + +void GlesPresenter::Resize(int outputW, int outputH) +{ + m_outputW = std::max(1, outputW); + m_outputH = std::max(1, outputH); + UpdateQuadVerts(); +} + +void GlesPresenter::UpdateFrameARGB(const uint32_t* pixelsARGB, int width, int height) +{ + if (!pixelsARGB || width <= 0 || height <= 0) + return; + + if (m_srcW != width || m_srcH != height) + { + m_srcW = width; + m_srcH = height; + glBindTexture(GL_TEXTURE_2D, m_tex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_srcW, m_srcH, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glBindTexture(GL_TEXTURE_2D, 0); + UpdateQuadVerts(); + } + + const size_t count = static_cast(width) * static_cast(height); + m_uploadRGBA.resize(count * 4); + + // Convert ARGB (0xAARRGGBB) -> RGBA8 bytes. + // Note: input is packed in CPU endian; treat as integer and extract. + for (size_t i = 0; i < count; ++i) + { + const uint32_t p = pixelsARGB[i]; + const uint8_t a = static_cast(p >> 24); + const uint8_t r = static_cast(p >> 16); + const uint8_t g = static_cast(p >> 8); + const uint8_t b = static_cast(p >> 0); + m_uploadRGBA[(i * 4) + 0] = r; + m_uploadRGBA[(i * 4) + 1] = g; + m_uploadRGBA[(i * 4) + 2] = b; + m_uploadRGBA[(i * 4) + 3] = a; + } + + glBindTexture(GL_TEXTURE_2D, m_tex); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_srcW, m_srcH, GL_RGBA, GL_UNSIGNED_BYTE, m_uploadRGBA.data()); + glBindTexture(GL_TEXTURE_2D, 0); +} + +void GlesPresenter::UpdateQuadVerts() +{ + if (m_outputW <= 0 || m_outputH <= 0 || m_srcW <= 0 || m_srcH <= 0) + return; + + const float outW = (float)m_outputW; + const float outH = (float)m_outputH; + const float srcW = (float)m_srcW; + const float srcH = (float)m_srcH; + + const float scale = std::min(outW / srcW, outH / srcH); + const float drawW = srcW * scale; + const float drawH = srcH * scale; + + const float ndcW = (drawW / outW) * 2.0f; + const float ndcH = (drawH / outH) * 2.0f; + + const float l = -ndcW * 0.5f; + const float r = +ndcW * 0.5f; + const float b = -ndcH * 0.5f; + const float t = +ndcH * 0.5f; + + // Triangle strip: (l,b)->(r,b)->(l,t)->(r,t) + // pos.xy, uv.xy + const float v[] = { + l, b, 0.0f, 1.0f, + r, b, 1.0f, 1.0f, + l, t, 0.0f, 0.0f, + r, t, 1.0f, 0.0f, + }; + std::memcpy(m_verts, v, sizeof(v)); + + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(m_verts), m_verts); + glBindBuffer(GL_ARRAY_BUFFER, 0); +} + +void GlesPresenter::Render() +{ + if (!m_program || !m_vao || !m_tex || m_outputW <= 0 || m_outputH <= 0) + return; + + glViewport(0, 0, m_outputW, m_outputH); + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + + glUseProgram(m_program); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_tex); + glUniform1i(m_uTex, 0); + + glBindVertexArray(m_vao); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + + glBindTexture(GL_TEXTURE_2D, 0); + glUseProgram(0); +} + diff --git a/android/app/src/main/cpp/gles_presenter.h b/android/app/src/main/cpp/gles_presenter.h new file mode 100644 index 0000000..3a449aa --- /dev/null +++ b/android/app/src/main/cpp/gles_presenter.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +// Minimal OpenGL ES presenter: +// - Uploads an ARGB8888 (0xAARRGGBB) framebuffer as a texture +// - Renders it with aspect-correct letterboxing +class GlesPresenter +{ +public: + bool Init(); + void Shutdown(); + + void Resize(int outputW, int outputH); + + // pixelsARGB: 0xAARRGGBB packed. + void UpdateFrameARGB(const uint32_t* pixelsARGB, int width, int height); + void Render(); + +private: + bool CreateProgram(); + bool CreateGeometry(); + void UpdateQuadVerts(); + + int m_outputW = 0; + int m_outputH = 0; + int m_srcW = 0; + int m_srcH = 0; + + unsigned m_program = 0; + unsigned m_vao = 0; + unsigned m_vbo = 0; + unsigned m_tex = 0; + int m_uTex = -1; + + // Interleaved: pos.xy, uv.xy (4 verts) + float m_verts[16]{}; + + // Converted upload buffer in RGBA8. + std::vector m_uploadRGBA; +}; + diff --git a/android/app/src/main/cpp/gles_stub_render3d.cpp b/android/app/src/main/cpp/gles_stub_render3d.cpp new file mode 100644 index 0000000..4ea70bb --- /dev/null +++ b/android/app/src/main/cpp/gles_stub_render3d.cpp @@ -0,0 +1,96 @@ +#include "gles_stub_render3d.h" + +#include + +namespace { +static GLuint Compile(GLenum type, const char* src) +{ + GLuint sh = glCreateShader(type); + glShaderSource(sh, 1, &src, nullptr); + glCompileShader(sh); + GLint ok = 0; + glGetShaderiv(sh, GL_COMPILE_STATUS, &ok); + if (!ok) + { + glDeleteShader(sh); + return 0; + } + return sh; +} +} // namespace + +void GlesStubRender3D::BeginFrame() +{ + if (m_inited) + return; + + static const char* vs = R"glsl( + #version 300 es + layout(location=0) in vec2 aPos; + void main() { gl_Position = vec4(aPos, 0.0, 1.0); } + )glsl"; + + static const char* fs = R"glsl( + #version 300 es + precision mediump float; + out vec4 oColor; + void main() { oColor = vec4(0.15, 0.2, 0.45, 1.0); } + )glsl"; + + GLuint v = Compile(GL_VERTEX_SHADER, vs); + GLuint f = Compile(GL_FRAGMENT_SHADER, fs); + if (!v || !f) + return; + + m_program = glCreateProgram(); + glAttachShader(m_program, v); + glAttachShader(m_program, f); + glLinkProgram(m_program); + glDeleteShader(v); + glDeleteShader(f); + + GLint ok = 0; + glGetProgramiv(m_program, GL_LINK_STATUS, &ok); + if (!ok) + { + glDeleteProgram(m_program); + m_program = 0; + return; + } + + glGenVertexArrays(1, &m_vao); + glGenBuffers(1, &m_vbo); + + // Small triangle in the top-left corner (NDC). + const float verts[] = { + -0.98f, 0.98f, + -0.70f, 0.98f, + -0.98f, 0.70f, + }; + + glBindVertexArray(m_vao); + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, (void*)0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + m_inited = true; +} + +void GlesStubRender3D::RenderFrame() +{ + if (!m_inited) + BeginFrame(); + if (!m_inited) + return; + + glUseProgram(m_program); + glBindVertexArray(m_vao); + glDisable(GL_DEPTH_TEST); + glDrawArrays(GL_TRIANGLES, 0, 3); + glBindVertexArray(0); + glUseProgram(0); +} + diff --git a/android/app/src/main/cpp/gles_stub_render3d.h b/android/app/src/main/cpp/gles_stub_render3d.h new file mode 100644 index 0000000..577425b --- /dev/null +++ b/android/app/src/main/cpp/gles_stub_render3d.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "Graphics/IRender3D.h" + +// Tiny GLES-backed IRender3D implementation used as a stepping stone: +// - Verifies the 3D renderer hook is being called from the Real3D/GPU path. +// - Does NOT implement Model 3 Real3D yet. +class GlesStubRender3D final : public IRender3D +{ +public: + bool Init(unsigned, unsigned, unsigned, unsigned, unsigned, unsigned) override { return true; } + void AttachMemory(const uint32_t*, const uint32_t*, const uint32_t*, const uint32_t*, const uint16_t*) override {} + void UploadTextures(unsigned, unsigned, unsigned, unsigned, unsigned) override {} + void SetStepping(int) override {} + void SetSunClamp(bool) override {} + void SetSignedShade(bool) override {} + float GetLosValue(int) override { return 0.f; } + + void BeginFrame() override; + void RenderFrame() override; + void EndFrame() override {} + +private: + bool m_inited = false; + GLuint m_program = 0; + GLuint m_vao = 0; + GLuint m_vbo = 0; +}; + diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp index 70b6cb9..c680740 100644 --- a/android/app/src/main/cpp/native-lib.cpp +++ b/android/app/src/main/cpp/native-lib.cpp @@ -10,6 +10,8 @@ #include #include +#include + #include "GameLoader.h" #include "Model3/Model3.h" #include "ROMSet.h" @@ -24,6 +26,8 @@ #include "BlockFile.h" #include "android_input_system.h" +#include "gles_presenter.h" +#include "gles_stub_render3d.h" // Minimal OSD glue ----------------------------------------------------------- @@ -58,7 +62,7 @@ struct Super3Host { AndroidInputSystem inputSystem; CInputs inputs{&inputSystem}; StubOutputs outputs; - NullRender3D render3d; + GlesStubRender3D render3d; CRender2D render2d{config}; std::unique_ptr loader; @@ -264,7 +268,6 @@ struct Super3Host { // it mainly keeps the input system in a sane state. inputs.Poll(&game, 0, 0, 496, 384); model3->RunFrame(); - model3->RenderFrame(); } } }; @@ -303,13 +306,20 @@ extern "C" int SDL_main(int argc, char* argv[]) { return 1; } + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + SDL_Window* window = SDL_CreateWindow( "Super3 (SDL bootstrap)", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, - SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE + SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL ); if (!window) { @@ -318,24 +328,21 @@ extern "C" int SDL_main(int argc, char* argv[]) { return 1; } - SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); - if (!renderer) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateRenderer failed: %s", SDL_GetError()); + SDL_GLContext gl = SDL_GL_CreateContext(window); + if (!gl) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GL_CreateContext failed: %s", SDL_GetError()); SDL_DestroyWindow(window); SDL_Quit(); return 1; } - // Texture for tile-generator output (496x384 ARGB). This renders the built-in test screen. - SDL_Texture* tgTexture = SDL_CreateTexture( - renderer, - SDL_PIXELFORMAT_ARGB8888, - SDL_TEXTUREACCESS_STREAMING, - 496, - 384); - if (!tgTexture) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateTexture failed: %s", SDL_GetError()); - SDL_DestroyRenderer(renderer); + SDL_GL_MakeCurrent(window, gl); + SDL_GL_SetSwapInterval(1); + + GlesPresenter presenter; + if (!presenter.Init()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GlesPresenter.Init failed"); + SDL_GL_DeleteContext(gl); SDL_DestroyWindow(window); SDL_Quit(); return 1; @@ -466,45 +473,27 @@ extern "C" int SDL_main(int argc, char* argv[]) { host.RunFrame(); } - // Present the tile generator framebuffer if available; otherwise clear to black. - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0xFF); - SDL_RenderClear(renderer); + int winW = 0, winH = 0; + SDL_GL_GetDrawableSize(window, &winW, &winH); + presenter.Resize(winW, winH); + + glClearColor(0.f, 0.f, 0.f, 1.f); + glClear(GL_COLOR_BUFFER_BIT); if (state == 1 && host.render2d.HasFrame()) { const uint32_t* pixels = host.render2d.GetFrameBufferRGBA(); - const int pitch = (int)(host.render2d.GetFrameWidth() * sizeof(uint32_t)); - if (SDL_UpdateTexture(tgTexture, nullptr, pixels, pitch) != 0) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_UpdateTexture failed: %s", SDL_GetError()); - } else { - int winW = 0, winH = 0; - SDL_GetRendererOutputSize(renderer, &winW, &winH); - const int srcW = (int)host.render2d.GetFrameWidth(); - const int srcH = (int)host.render2d.GetFrameHeight(); + presenter.UpdateFrameARGB(pixels, (int)host.render2d.GetFrameWidth(), (int)host.render2d.GetFrameHeight()); + presenter.Render(); - // Preserve aspect ratio (letterbox/pillarbox). - float scale = std::min((float)winW / (float)srcW, (float)winH / (float)srcH); - SDL_Rect dst{}; - dst.w = (int)(srcW * scale); - dst.h = (int)(srcH * scale); - dst.x = (winW - dst.w) / 2; - dst.y = (winH - dst.h) / 2; - SDL_RenderCopy(renderer, tgTexture, nullptr, &dst); - } + // Debug: draw a small triangle on top so we can verify GLES is working + // and we have a valid 3D hook. The real integration will happen inside + // the GPU/TileGen render path once New3D is ported. + host.render3d.BeginFrame(); + host.render3d.RenderFrame(); + host.render3d.EndFrame(); } - SDL_RenderPresent(renderer); - if (SDL_GetError()[0] != '\0') { - // SDL doesn't always surface present errors via return codes; log and clear. - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL error after present: %s", SDL_GetError()); - // If the EGL surface has gone away (background/minimize), stop rendering - // until we get a foreground/surface recreation event. - if (strstr(SDL_GetError(), "EGL_BAD_SURFACE") != nullptr || - strstr(SDL_GetError(), "unable to show color buffer") != nullptr) { - backgrounded = true; - SDL_Log("Detected bad surface; pausing rendering until foreground"); - } - SDL_ClearError(); - } + SDL_GL_SwapWindow(window); uint32_t t = SDL_GetTicks(); if (t - lastStatusLog > 2000) { @@ -513,12 +502,12 @@ extern "C" int SDL_main(int argc, char* argv[]) { } } - SDL_DestroyTexture(tgTexture); + presenter.Shutdown(); if (loadState.load(std::memory_order_acquire) == 1) { host.SaveNVRAM(); } CloseAudio(); - SDL_DestroyRenderer(renderer); + SDL_GL_DeleteContext(gl); SDL_DestroyWindow(window); SDL_Quit(); return 0;