diff --git a/.github/gles.patch b/.github/gles.patch
index 548b243d..b16733b5 100644
--- a/.github/gles.patch
+++ b/.github/gles.patch
@@ -1,25 +1,3 @@
-diff --git a/src/host_shaders/opengl_display.frag b/src/host_shaders/opengl_display.frag
-index 612671c8..1937f711 100644
---- a/src/host_shaders/opengl_display.frag
-+++ b/src/host_shaders/opengl_display.frag
-@@ -1,4 +1,5 @@
--#version 410 core
-+#version 300 es
-+precision mediump float;
- in vec2 UV;
- out vec4 FragColor;
- 
-diff --git a/src/host_shaders/opengl_display.vert b/src/host_shaders/opengl_display.vert
-index 990e2f80..2e7842ac 100644
---- a/src/host_shaders/opengl_display.vert
-+++ b/src/host_shaders/opengl_display.vert
-@@ -1,4 +1,5 @@
--#version 410 core
-+#version 300 es
-+precision mediump float;
- out vec2 UV;
- 
- void main() {
 diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag
 index 9f07df0b..96a35afa 100644
 --- a/src/host_shaders/opengl_fragment_shader.frag
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7bd8b9ed..0f4fdb13 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -465,8 +465,9 @@ if(ENABLE_OPENGL)
 
     set(RENDERER_GL_SOURCE_FILES src/core/renderer_gl/renderer_gl.cpp
         src/core/renderer_gl/textures.cpp src/core/renderer_gl/etc1.cpp
-        src/core/renderer_gl/gl_state.cpp src/host_shaders/opengl_display.frag
-        src/host_shaders/opengl_display.vert src/host_shaders/opengl_vertex_shader.vert
+        src/core/renderer_gl/gl_state.cpp src/host_shaders/opengl_display.vert
+        src/host_shaders/opengl_display.frag src/host_shaders/opengl_es_display.vert
+        src/host_shaders/opengl_es_display.frag src/host_shaders/opengl_vertex_shader.vert
         src/host_shaders/opengl_fragment_shader.frag
     )
 
@@ -479,8 +480,10 @@ if(ENABLE_OPENGL)
         resources_renderer_gl
         NAMESPACE RendererGL
         WHENCE "src/host_shaders/"
-        "src/host_shaders/opengl_display.frag"
         "src/host_shaders/opengl_display.vert"
+        "src/host_shaders/opengl_display.frag"
+        "src/host_shaders/opengl_es_display.vert"
+        "src/host_shaders/opengl_es_display.frag"
         "src/host_shaders/opengl_vertex_shader.vert"
         "src/host_shaders/opengl_fragment_shader.frag"
     )
diff --git a/include/emulator.hpp b/include/emulator.hpp
index cf231328..a222a021 100644
--- a/include/emulator.hpp
+++ b/include/emulator.hpp
@@ -89,7 +89,6 @@ class Emulator {
 	~Emulator();
 
 	void step();
-	void render();
 	void reset(ReloadOption reload);
 	void runFrame();
 	// Poll the scheduler for events
diff --git a/include/renderer.hpp b/include/renderer.hpp
index bc5dfac6..b458ecce 100644
--- a/include/renderer.hpp
+++ b/include/renderer.hpp
@@ -81,6 +81,10 @@ class Renderer {
 	virtual std::string getUbershader() { return ""; }
 	virtual void setUbershader(const std::string& shader) {}
 
+	// Only relevant for OpenGL renderer and other OpenGL-based backends (eg software)
+	// Called to notify the core to use OpenGL ES and not desktop GL
+	virtual void setupGLES() {}
+
 	// This function is called on every draw call before parsing vertex data.
 	// It is responsible for things like looking up which vertex/fragment shaders to use, recompiling them if they don't exist, choosing between
 	// ubershaders and shadergen, and so on.
diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp
index fab239f2..a862cd26 100644
--- a/include/renderer_gl/renderer_gl.hpp
+++ b/include/renderer_gl/renderer_gl.hpp
@@ -40,7 +40,7 @@ class RendererGL final : public Renderer {
 	OpenGL::VertexArray hwShaderVAO;
 	OpenGL::VertexBuffer vbo;
 
-	// Data 
+	// Data
 	struct {
 		// TEV configuration uniform locations
 		GLint textureEnvSourceLoc = -1;
@@ -157,6 +157,7 @@ class RendererGL final : public Renderer {
 	void initGraphicsContextInternal();
 
 	void accelerateVertexUpload(ShaderUnit& shaderUnit, PICA::DrawAcceleration* accel);
+	void compileDisplayShader();
 
   public:
 	RendererGL(GPU& gpu, const std::array<u32, regNum>& internalRegs, const std::array<u32, extRegNum>& externalRegs)
@@ -169,14 +170,15 @@ class RendererGL final : public Renderer {
 	void clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) override;  // Clear a GPU buffer in VRAM
 	void displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) override;  // Perform display transfer
 	void textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) override;
-	void drawVertices(PICA::PrimType primType, std::span<const PICA::Vertex> vertices) override;             // Draw the given vertices
+	void drawVertices(PICA::PrimType primType, std::span<const PICA::Vertex> vertices) override;  // Draw the given vertices
 	void deinitGraphicsContext() override;
 
 	virtual bool supportsShaderReload() override { return true; }
 	virtual std::string getUbershader() override;
 	virtual void setUbershader(const std::string& shader) override;
 	virtual bool prepareForDraw(ShaderUnit& shaderUnit, PICA::DrawAcceleration* accel) override;
-	
+	virtual void setupGLES() override;
+
 	std::optional<ColourBuffer> getColourBuffer(u32 addr, PICA::ColorFmt format, u32 width, u32 height, bool createIfnotFound = true);
 
 	// Note: The caller is responsible for deleting the currently bound FBO before calling this
diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp
index 35de68ce..ab44eb08 100644
--- a/src/core/renderer_gl/renderer_gl.cpp
+++ b/src/core/renderer_gl/renderer_gl.cpp
@@ -56,10 +56,6 @@ void RendererGL::reset() {
 void RendererGL::initGraphicsContextInternal() {
 	gl.reset();
 
-#if defined(USING_GLES) || defined(__ANDROID__)
-	driverInfo.usingGLES = true;
-#endif
-
 	auto gl_resources = cmrc::RendererGL::get_filesystem();
 	auto vertexShaderSource = gl_resources.open("opengl_vertex_shader.vert");
 	auto fragmentShaderSource = gl_resources.open("opengl_fragment_shader.frag");
@@ -69,16 +65,7 @@ void RendererGL::initGraphicsContextInternal() {
 	triangleProgram.create({vert, frag});
 	initUbershader(triangleProgram);
 
-	auto displayVertexShaderSource = gl_resources.open("opengl_display.vert");
-	auto displayFragmentShaderSource = gl_resources.open("opengl_display.frag");
-
-	OpenGL::Shader vertDisplay({displayVertexShaderSource.begin(), displayVertexShaderSource.size()}, OpenGL::Vertex);
-	OpenGL::Shader fragDisplay({displayFragmentShaderSource.begin(), displayFragmentShaderSource.size()}, OpenGL::Fragment);
-	displayProgram.create({vertDisplay, fragDisplay});
-
-	gl.useProgram(displayProgram);
-	glUniform1i(OpenGL::uniformLocation(displayProgram, "u_texture"), 0);  // Init sampler object
-
+	compileDisplayShader();
 	// Create stream buffers for vertex, index and uniform buffers
 	static constexpr usize hwIndexBufferSize = 2_MB;
 	static constexpr usize hwVertexBufferSize = 16_MB;
@@ -1156,6 +1143,19 @@ void RendererGL::initUbershader(OpenGL::Program& program) {
 	glUniform1i(OpenGL::uniformLocation(program, "u_tex_luts"), 3);
 }
 
+void RendererGL::compileDisplayShader() {
+	auto gl_resources = cmrc::RendererGL::get_filesystem();
+	auto displayVertexShaderSource = driverInfo.usingGLES ? gl_resources.open("opengl_es_display.vert") : gl_resources.open("opengl_display.vert");
+	auto displayFragmentShaderSource = driverInfo.usingGLES ? gl_resources.open("opengl_es_display.frag") : gl_resources.open("opengl_display.frag");
+
+	OpenGL::Shader vertDisplay({displayVertexShaderSource.begin(), displayVertexShaderSource.size()}, OpenGL::Vertex);
+	OpenGL::Shader fragDisplay({displayFragmentShaderSource.begin(), displayFragmentShaderSource.size()}, OpenGL::Fragment);
+	displayProgram.create({vertDisplay, fragDisplay});
+
+	gl.useProgram(displayProgram);
+	glUniform1i(OpenGL::uniformLocation(displayProgram, "u_texture"), 0);  // Init sampler object
+}
+
 void RendererGL::accelerateVertexUpload(ShaderUnit& shaderUnit, PICA::DrawAcceleration* accel) {
 	u32 buffer = 0;  // Vertex buffer index for non-fixed attributes
 	u32 attrCount = 0;
@@ -1250,4 +1250,18 @@ void RendererGL::accelerateVertexUpload(ShaderUnit& shaderUnit, PICA::DrawAccele
 			);
 		}
 	}
+}
+
+void RendererGL::setupGLES() {
+	driverInfo.usingGLES = true;
+
+	// OpenGL ES hardware is typically way too slow to use the ubershader (eg RPi, mobile phones, handhelds) or has other issues with it.
+	// So, display a warning and turn them off on OpenGL ES.
+	if (emulatorConfig->useUbershaders) {
+		emulatorConfig->useUbershaders = false;
+		Helpers::warn("Ubershaders enabled on OpenGL ES. This usually results in a worse experience, turning it off...");
+	}
+
+	// Stub out logic operations so that calling them doesn't crash the emulator
+	glLogicOp = [](GLenum) {};
 }
\ No newline at end of file
diff --git a/src/emulator.cpp b/src/emulator.cpp
index 81a18f30..11970d91 100644
--- a/src/emulator.cpp
+++ b/src/emulator.cpp
@@ -117,7 +117,6 @@ std::filesystem::path Emulator::getConfigPath() {
 #endif
 
 void Emulator::step() {}
-void Emulator::render() {}
 
 // Only resume if a ROM is properly loaded
 void Emulator::resume() {
diff --git a/src/host_shaders/opengl_es_display.frag b/src/host_shaders/opengl_es_display.frag
new file mode 100644
index 00000000..600ebfcd
--- /dev/null
+++ b/src/host_shaders/opengl_es_display.frag
@@ -0,0 +1,10 @@
+#version 310 es
+precision mediump float;
+
+in vec2 UV;
+out vec4 FragColor;
+
+uniform sampler2D u_texture;
+void main() {
+	FragColor = texture(u_texture, UV);
+}
\ No newline at end of file
diff --git a/src/host_shaders/opengl_es_display.vert b/src/host_shaders/opengl_es_display.vert
new file mode 100644
index 00000000..04fadfc6
--- /dev/null
+++ b/src/host_shaders/opengl_es_display.vert
@@ -0,0 +1,25 @@
+#version 310 es
+precision mediump float;
+
+out vec2 UV;
+
+void main() {
+	const vec4 positions[4] = vec4[](
+		vec4(-1.0, 1.0, 1.0, 1.0),   // Top-left
+		vec4(1.0, 1.0, 1.0, 1.0),    // Top-right
+		vec4(-1.0, -1.0, 1.0, 1.0),  // Bottom-left
+		vec4(1.0, -1.0, 1.0, 1.0)    // Bottom-right
+	);
+
+	// The 3DS displays both screens' framebuffer rotated 90 deg counter clockwise
+	// So we adjust our texcoords accordingly
+	const vec2 texcoords[4] = vec2[](
+		vec2(1.0, 1.0),  // Top-right
+		vec2(1.0, 0.0),  // Bottom-right
+		vec2(0.0, 1.0),  // Top-left
+		vec2(0.0, 0.0)   // Bottom-left
+	);
+
+	gl_Position = positions[gl_VertexID];
+	UV = texcoords[gl_VertexID];
+}
\ No newline at end of file
diff --git a/src/hydra_core.cpp b/src/hydra_core.cpp
index 078b8a6c..0bcd21a8 100644
--- a/src/hydra_core.cpp
+++ b/src/hydra_core.cpp
@@ -118,6 +118,7 @@ void HydraCore::resetContext() {
 	if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(getProcAddress))) {
 		Helpers::panic("OpenGL ES init failed");
 	}
+	emulator->getRenderer()->setupGLES();
 #else
 	if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(getProcAddress))) {
 		Helpers::panic("OpenGL init failed");
diff --git a/src/jni_driver.cpp b/src/jni_driver.cpp
index 60bbc680..6a156360 100644
--- a/src/jni_driver.cpp
+++ b/src/jni_driver.cpp
@@ -78,6 +78,7 @@ AlberFunction(void, Initialize)(JNIEnv* env, jobject obj) {
 	}
 
 	__android_log_print(ANDROID_LOG_INFO, "AlberDriver", "OpenGL ES %d.%d", GLVersion.major, GLVersion.minor);
+	emulator->getRenderer()->setupGLES();
 	emulator->initGraphicsContext(nullptr);
 }
 
@@ -153,7 +154,6 @@ int AndroidUtils::openDocument(const char* path, const char* perms) {
 
     jstring uri = env->NewStringUTF(path);
     jstring jmode = env->NewStringUTF(perms);
-
     jint result = env->CallStaticIntMethod(alberClass, alberClassOpenDocument, uri, jmode);
 
     env->DeleteLocalRef(uri);
diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp
index fe3cb6c4..727da8d2 100644
--- a/src/libretro_core.cpp
+++ b/src/libretro_core.cpp
@@ -17,7 +17,8 @@ static retro_input_state_t inputStateCallback;
 static retro_hw_render_callback hwRender;
 static std::filesystem::path savePath;
 
-static bool screenTouched;
+static bool screenTouched = false;
+static bool usingGLES = false;
 
 std::unique_ptr<Emulator> emulator;
 RendererGL* renderer;
@@ -35,15 +36,19 @@ static void* getGLProcAddress(const char* name) {
 }
 
 static void videoResetContext() {
-#ifdef USING_GLES
-	if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(getGLProcAddress))) {
-		Helpers::panic("OpenGL ES init failed");
+	if (usingGLES) {
+		if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(getGLProcAddress))) {
+			Helpers::panic("OpenGL ES init failed");
+		}
+
+		emulator->getRenderer()->setupGLES();
 	}
-#else
-	if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(getGLProcAddress))) {
-		Helpers::panic("OpenGL init failed");
+
+	else {
+		if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(getGLProcAddress))) {
+			Helpers::panic("OpenGL init failed");
+		}
 	}
-#endif
 
 	emulator->initGraphicsContext(nullptr);
 }
@@ -73,6 +78,7 @@ static bool setHWRender(retro_hw_context_type type) {
 			hwRender.version_minor = 1;
 
 			if (envCallback(RETRO_ENVIRONMENT_SET_HW_RENDER, &hwRender)) {
+				usingGLES = true;
 				return true;
 			}
 			break;
diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp
index 881dc02d..dffe5ca0 100644
--- a/src/panda_qt/main_window.cpp
+++ b/src/panda_qt/main_window.cpp
@@ -140,6 +140,10 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
 			glContext->MakeCurrent();
 			glContext->SetSwapInterval(emu->getConfig().vsyncEnabled ? 1 : 0);
 
+			if (glContext->IsGLES()) {
+				emu->getRenderer()->setupGLES();
+			}
+
 			emu->initGraphicsContext(glContext);
 		} else if (usingVk) {
 			Helpers::panic("Vulkan on Qt is currently WIP, try the SDL frontend instead!");
diff --git a/src/panda_qt/screen.cpp b/src/panda_qt/screen.cpp
index 25ff576c..919b6694 100644
--- a/src/panda_qt/screen.cpp
+++ b/src/panda_qt/screen.cpp
@@ -60,11 +60,12 @@ void ScreenWidget::resizeSurface(u32 width, u32 height) {
 }
 
 bool ScreenWidget::createGLContext() {
-	// List of GL context versions we will try. Anything 4.1+ is good
-	static constexpr std::array<GL::Context::Version, 6> versionsToTry = {
+	// List of GL context versions we will try. Anything 4.1+ is good for desktop OpenGL, and 3.1+ for OpenGL ES
+	static constexpr std::array<GL::Context::Version, 8> versionsToTry = {
 		GL::Context::Version{GL::Context::Profile::Core, 4, 6}, GL::Context::Version{GL::Context::Profile::Core, 4, 5},
 		GL::Context::Version{GL::Context::Profile::Core, 4, 4}, GL::Context::Version{GL::Context::Profile::Core, 4, 3},
 		GL::Context::Version{GL::Context::Profile::Core, 4, 2}, GL::Context::Version{GL::Context::Profile::Core, 4, 1},
+		GL::Context::Version{GL::Context::Profile::ES, 3, 2},   GL::Context::Version{GL::Context::Profile::ES, 3, 1},
 	};
 
 	std::optional<WindowInfo> windowInfo = getWindowInfo();
@@ -72,6 +73,10 @@ bool ScreenWidget::createGLContext() {
 		this->windowInfo = *windowInfo;
 
 		glContext = GL::Context::Create(*getWindowInfo(), versionsToTry);
+		if (glContext == nullptr) {
+			return false;
+		}
+
 		glContext->DoneCurrent();
 	}
 
diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp
index 1c07c25e..2d60d2fa 100644
--- a/src/panda_sdl/frontend_sdl.cpp
+++ b/src/panda_sdl/frontend_sdl.cpp
@@ -71,11 +71,27 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp
 
 		glContext = SDL_GL_CreateContext(window);
 		if (glContext == nullptr) {
-			Helpers::panic("OpenGL context creation failed: %s", SDL_GetError());
-		}
+			Helpers::warn("OpenGL context creation failed: %s\nTrying again with OpenGL ES.", SDL_GetError());
 
-		if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
-			Helpers::panic("OpenGL init failed");
+			// Some low end devices (eg RPi, emulation handhelds) don't support desktop GL, but only OpenGL ES, so fall back to that if GL context
+			// creation failed
+			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, 1);
+			glContext = SDL_GL_CreateContext(window);
+			if (glContext == nullptr) {
+				Helpers::panic("OpenGL context creation failed: %s", SDL_GetError());
+			}
+
+			if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
+				Helpers::panic("OpenGL init failed");
+			}
+
+			emu.getRenderer()->setupGLES();
+		} else {
+			if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
+				Helpers::panic("OpenGL init failed");
+			}
 		}
 
 		SDL_GL_SetSwapInterval(config.vsyncEnabled ? 1 : 0);