Browse Source

Render: refactor to Backend + DrawQueue; rename renderer -> scene_renderer; add GroundRenderer module and integrate into load/render; update build files and entity renderers

djeada 2 months ago
parent
commit
dc48c527f2

+ 14 - 3
app/game_engine.cpp

@@ -7,9 +7,10 @@
 
 
 #include "game/core/world.h"
 #include "game/core/world.h"
 #include "game/core/component.h"
 #include "game/core/component.h"
-#include "render/gl/renderer.h"
+#include "render/scene_renderer.h"
 #include "render/gl/camera.h"
 #include "render/gl/camera.h"
 #include "render/gl/resources.h"
 #include "render/gl/resources.h"
+#include "render/ground/ground_renderer.h"
 #include "render/geom/arrow.h"
 #include "render/geom/arrow.h"
 #include "render/gl/bootstrap.h"
 #include "render/gl/bootstrap.h"
 #include "game/map/level_loader.h"
 #include "game/map/level_loader.h"
@@ -33,6 +34,7 @@ GameEngine::GameEngine() {
     m_world    = std::make_unique<Engine::Core::World>();
     m_world    = std::make_unique<Engine::Core::World>();
     m_renderer = std::make_unique<Render::GL::Renderer>();
     m_renderer = std::make_unique<Render::GL::Renderer>();
     m_camera   = std::make_unique<Render::GL::Camera>();
     m_camera   = std::make_unique<Render::GL::Camera>();
+    m_ground   = std::make_unique<Render::GL::GroundRenderer>();
 
 
     std::unique_ptr<Engine::Core::System> arrowSys = std::make_unique<Game::Systems::ArrowSystem>();
     std::unique_ptr<Engine::Core::System> arrowSys = std::make_unique<Game::Systems::ArrowSystem>();
     m_arrowSystem = static_cast<Game::Systems::ArrowSystem*>(arrowSys.get());
     m_arrowSystem = static_cast<Game::Systems::ArrowSystem*>(arrowSys.get());
@@ -121,11 +123,15 @@ void GameEngine::onAreaSelected(qreal x1, qreal y1, qreal x2, qreal y2, bool add
 }
 }
 
 
 void GameEngine::initialize() {
 void GameEngine::initialize() {
-    if (!Render::GL::RenderBootstrap::initialize(*m_renderer, *m_camera, m_resources)) {
+    if (!Render::GL::RenderBootstrap::initialize(*m_renderer, *m_camera)) {
         return;
         return;
     }
     }
     QString mapPath = QString::fromUtf8("assets/maps/test_map.json");
     QString mapPath = QString::fromUtf8("assets/maps/test_map.json");
     auto lr = Game::Map::LevelLoader::loadFromAssets(mapPath, *m_world, *m_renderer, *m_camera);
     auto lr = Game::Map::LevelLoader::loadFromAssets(mapPath, *m_world, *m_renderer, *m_camera);
+    if (m_ground) {
+        if (lr.ok) m_ground->configure(lr.tileSize, lr.gridWidth, lr.gridHeight);
+        else m_ground->configureExtent(50.0f);
+    }
     m_level.mapName = lr.mapName;
     m_level.mapName = lr.mapName;
     m_level.playerUnitId = lr.playerUnitId;
     m_level.playerUnitId = lr.playerUnitId;
     m_level.camFov = lr.camFov; m_level.camNear = lr.camNear; m_level.camFar = lr.camFar;
     m_level.camFov = lr.camFov; m_level.camNear = lr.camNear; m_level.camFar = lr.camFar;
@@ -161,9 +167,14 @@ void GameEngine::render(int pixelWidth, int pixelHeight) {
         m_renderer->setSelectedEntities(ids);
         m_renderer->setSelectedEntities(ids);
     }
     }
     m_renderer->beginFrame();
     m_renderer->beginFrame();
+    if (m_ground && m_renderer) {
+        if (auto* res = m_renderer->resources()) m_ground->submit(*m_renderer, *res);
+    }
     if (m_renderer) m_renderer->setHoveredBuildingId(m_hover.buildingId);
     if (m_renderer) m_renderer->setHoveredBuildingId(m_hover.buildingId);
     m_renderer->renderWorld(m_world.get());
     m_renderer->renderWorld(m_world.get());
-    if (m_arrowSystem) { Render::GL::renderArrows(m_renderer.get(), m_resources.get(), *m_arrowSystem); }
+    if (m_arrowSystem) {
+        if (auto* res = m_renderer->resources()) Render::GL::renderArrows(m_renderer.get(), res, *m_arrowSystem);
+    }
     m_renderer->endFrame();
     m_renderer->endFrame();
 }
 }
 
 

+ 3 - 1
app/game_engine.h

@@ -21,6 +21,7 @@ namespace Render { namespace GL {
 class Renderer;
 class Renderer;
 class Camera;
 class Camera;
 class ResourceManager;
 class ResourceManager;
+class GroundRenderer;
 } }
 } }
 
 
 namespace Game { namespace Systems { class SelectionSystem; class ArrowSystem; class PickingService; } }
 namespace Game { namespace Systems { class SelectionSystem; class ArrowSystem; class PickingService; } }
@@ -86,7 +87,8 @@ private:
     std::unique_ptr<Engine::Core::World> m_world;
     std::unique_ptr<Engine::Core::World> m_world;
     std::unique_ptr<Render::GL::Renderer> m_renderer;
     std::unique_ptr<Render::GL::Renderer> m_renderer;
     std::unique_ptr<Render::GL::Camera>   m_camera;
     std::unique_ptr<Render::GL::Camera>   m_camera;
-    std::shared_ptr<Render::GL::ResourceManager> m_resources;
+    std::shared_ptr<Render::GL::ResourceManager> m_resources; // deprecated; will be unused with backend-owned resources
+    std::unique_ptr<Render::GL::GroundRenderer> m_ground;
     std::unique_ptr<Game::Systems::SelectionSystem> m_selectionSystem;
     std::unique_ptr<Game::Systems::SelectionSystem> m_selectionSystem;
     std::unique_ptr<Game::Systems::PickingService> m_pickingService;
     std::unique_ptr<Game::Systems::PickingService> m_pickingService;
     QQuickWindow* m_window = nullptr;
     QQuickWindow* m_window = nullptr;

+ 26 - 4
assets/shaders/grid.frag

@@ -11,12 +11,34 @@ uniform float u_thickness; // fraction of cell (0..0.5)
 
 
 out vec4 FragColor;
 out vec4 FragColor;
 
 
+// Hash for subtle per-cell variation
+float hash12(vec2 p) {
+    vec3 p3 = fract(vec3(p.xyx) * 0.1031);
+    p3 += dot(p3, p3.yzx + 33.33);
+    return fract((p3.x + p3.y) * p3.z);
+}
+
 void main() {
 void main() {
     vec2 coord = v_worldPos.xz / u_cellSize;
     vec2 coord = v_worldPos.xz / u_cellSize;
-    vec2 g = abs(fract(coord) - 0.5);
-    float lineX = step(0.5 - u_thickness, g.x);
-    float lineY = step(0.5 - u_thickness, g.y);
+    vec2 f = fract(coord) - 0.5;
+    vec2 af = abs(f);
+
+    // Anti-aliased lines using fwidth
+    float fw = fwidth(af.x);
+    float lineX = 1.0 - smoothstep(0.5 - u_thickness - fw, 0.5 - u_thickness + fw, af.x);
+    fw = fwidth(af.y);
+    float lineY = 1.0 - smoothstep(0.5 - u_thickness - fw, 0.5 - u_thickness + fw, af.y);
     float lineMask = max(lineX, lineY);
     float lineMask = max(lineX, lineY);
-    vec3 col = mix(u_gridColor, u_lineColor, lineMask);
+
+    // Emphasize major lines every 5 cells
+    vec2 cell = floor(coord);
+    float major = (abs(mod(cell.x, 5.0)) < 0.5 || abs(mod(cell.y, 5.0)) < 0.5) ? 1.0 : 0.0;
+    vec3 lineCol = mix(u_lineColor, u_lineColor * 1.2, major);
+
+    // Subtle per-cell brightness jitter
+    float jitter = (hash12(cell) - 0.5) * 0.06;
+    vec3 base = u_gridColor * (1.0 + jitter);
+
+    vec3 col = mix(base, lineCol, lineMask);
     FragColor = vec4(col, 1.0);
     FragColor = vec4(col, 1.0);
 }
 }

+ 1 - 1
game/map/environment.cpp

@@ -1,5 +1,5 @@
 #include "environment.h"
 #include "environment.h"
-#include "../../render/gl/renderer.h"
+#include "../../render/scene_renderer.h"
 #include "../../render/gl/camera.h"
 #include "../../render/gl/camera.h"
 #include <algorithm>
 #include <algorithm>
 
 

+ 6 - 4
game/map/level_loader.cpp

@@ -6,7 +6,7 @@
 #include "../units/factory.h"
 #include "../units/factory.h"
 #include "../core/world.h"
 #include "../core/world.h"
 #include "../core/component.h"
 #include "../core/component.h"
-#include "../../render/gl/renderer.h"
+#include "../../render/scene_renderer.h"
 #include "../../render/gl/camera.h"
 #include "../../render/gl/camera.h"
 #include <QDebug>
 #include <QDebug>
 
 
@@ -33,10 +33,11 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString& mapPath,
     QString err;
     QString err;
     if (Game::Map::MapLoader::loadFromJsonFile(mapPath, def, &err)) {
     if (Game::Map::MapLoader::loadFromJsonFile(mapPath, def, &err)) {
         res.ok = true;
         res.ok = true;
-        res.mapName = def.name;
+    res.mapName = def.name;
         // Apply environment
         // Apply environment
         Game::Map::Environment::apply(def, renderer, camera);
         Game::Map::Environment::apply(def, renderer, camera);
-        res.camFov = def.camera.fovY; res.camNear = def.camera.nearPlane; res.camFar = def.camera.farPlane;
+    res.camFov = def.camera.fovY; res.camNear = def.camera.nearPlane; res.camFar = def.camera.farPlane;
+    res.gridWidth = def.grid.width; res.gridHeight = def.grid.height; res.tileSize = def.grid.tileSize;
         // Populate world
         // Populate world
         auto rt = Game::Map::MapTransformer::applyToWorld(def, world, &visualCatalog);
         auto rt = Game::Map::MapTransformer::applyToWorld(def, world, &visualCatalog);
         if (!rt.unitIds.empty()) {
         if (!rt.unitIds.empty()) {
@@ -70,8 +71,9 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString& mapPath,
     } else {
     } else {
         qWarning() << "LevelLoader: Map load failed:" << err << " - applying default environment";
         qWarning() << "LevelLoader: Map load failed:" << err << " - applying default environment";
         Game::Map::Environment::applyDefault(renderer, camera);
         Game::Map::Environment::applyDefault(renderer, camera);
-        res.ok = false;
+    res.ok = false;
         res.camFov = camera.getFOV(); res.camNear = camera.getNear(); res.camFar = camera.getFar();
         res.camFov = camera.getFOV(); res.camNear = camera.getNear(); res.camFar = camera.getFar();
+    res.gridWidth = 50; res.gridHeight = 50; res.tileSize = 1.0f;
         // Fallback archer spawn as last resort
         // Fallback archer spawn as last resort
         auto reg = Game::Map::MapTransformer::getFactoryRegistry();
         auto reg = Game::Map::MapTransformer::getFactoryRegistry();
         if (reg) {
         if (reg) {

+ 3 - 0
game/map/level_loader.h

@@ -15,6 +15,9 @@ struct LevelLoadResult {
     float camFov = 45.0f;
     float camFov = 45.0f;
     float camNear = 0.1f;
     float camNear = 0.1f;
     float camFar = 1000.0f;
     float camFar = 1000.0f;
+    int gridWidth = 50;
+    int gridHeight = 50;
+    float tileSize = 1.0f;
 };
 };
 
 
 class LevelLoader {
 class LevelLoader {

+ 1 - 1
game/systems/arrow_system.cpp

@@ -1,5 +1,5 @@
 #include "arrow_system.h"
 #include "arrow_system.h"
-#include "../../render/gl/renderer.h"
+#include "../../render/scene_renderer.h"
 #include "../../render/gl/resources.h"
 #include "../../render/gl/resources.h"
 #include "../../render/geom/arrow.h"
 #include "../../render/geom/arrow.h"
 #include <algorithm>
 #include <algorithm>

+ 7 - 1
render/CMakeLists.txt

@@ -3,15 +3,21 @@ add_library(render_gl STATIC
     gl/buffer.cpp
     gl/buffer.cpp
     gl/mesh.cpp
     gl/mesh.cpp
     gl/texture.cpp
     gl/texture.cpp
-    gl/renderer.cpp
+    scene_renderer.cpp
     gl/camera.cpp
     gl/camera.cpp
     gl/resources.cpp
     gl/resources.cpp
     gl/bootstrap.cpp
     gl/bootstrap.cpp
+    gl/backend.cpp
+    gl/shader_cache.cpp
+    gl/state_scopes.cpp
+    draw_queue.cpp
+    ground/ground_renderer.cpp
     entity/registry.cpp
     entity/registry.cpp
     entity/archer_renderer.cpp
     entity/archer_renderer.cpp
     entity/barracks_renderer.cpp
     entity/barracks_renderer.cpp
     # entity/arrow.cpp removed; arrow VFX renderer code moved to geom/arrow.cpp
     # entity/arrow.cpp removed; arrow VFX renderer code moved to geom/arrow.cpp
     geom/selection_ring.cpp
     geom/selection_ring.cpp
+    geom/selection_disc.cpp
     geom/arrow.cpp
     geom/arrow.cpp
 )
 )
 
 

+ 7 - 0
render/draw_queue.cpp

@@ -0,0 +1,7 @@
+#include "draw_queue.h"
+namespace Render::GL {
+// Intentionally minimal; logic can be extended later
+}
+namespace Render {
+// Intentionally empty: DrawQueue is a POD-like container defined in the header.
+}

+ 72 - 0
render/draw_queue.h

@@ -0,0 +1,72 @@
+#pragma once
+
+#include <vector>
+#include <variant>
+#include <algorithm>
+#include <QMatrix4x4>
+#include <QVector3D>
+
+namespace Render::GL { class Mesh; class Texture; }
+
+namespace Render::GL {
+
+struct MeshCmd {
+    Mesh* mesh = nullptr;
+    Texture* texture = nullptr;
+    QMatrix4x4 model;
+    QVector3D color{1,1,1};
+    float alpha = 1.0f;
+};
+
+struct GridCmd {
+    // Placeholder for future grid overlay; keep minimal to avoid churn now
+    QMatrix4x4 model;
+    QVector3D color{0.2f,0.25f,0.2f};
+    float cellSize = 1.0f;
+    float thickness = 0.06f;
+    float extent = 50.0f;
+};
+
+struct SelectionRingCmd {
+    QMatrix4x4 model;
+    QVector3D color{0,0,0};
+    float alphaInner = 0.6f;
+    float alphaOuter = 0.25f;
+};
+
+struct SelectionSmokeCmd {
+    QMatrix4x4 model;
+    QVector3D color{1,1,1};
+    float baseAlpha = 0.15f; // alpha for the innermost layer; outer layers fade down
+};
+
+using DrawCmd = std::variant<MeshCmd, GridCmd, SelectionRingCmd, SelectionSmokeCmd>;
+
+class DrawQueue {
+public:
+    void clear() { m_items.clear(); }
+    void submit(const MeshCmd& c) { m_items.emplace_back(c); }
+    void submit(const GridCmd& c) { m_items.emplace_back(c); }
+    void submit(const SelectionRingCmd& c) { m_items.emplace_back(c); }
+    void submit(const SelectionSmokeCmd& c) { m_items.emplace_back(c); }
+    bool empty() const { return m_items.empty(); }
+    const std::vector<DrawCmd>& items() const { return m_items; }
+    std::vector<DrawCmd>& items() { return m_items; }
+    void sortForBatching() {
+        // Order: Grid first (background), then Mesh, then SelectionRing (foreground overlays)
+        auto weight = [](const DrawCmd& c) -> int {
+            if (std::holds_alternative<GridCmd>(c)) return 0;             // ground
+            if (std::holds_alternative<SelectionSmokeCmd>(c)) return 1;    // smoke base under meshes
+            if (std::holds_alternative<MeshCmd>(c)) return 2;              // entities
+            if (std::holds_alternative<SelectionRingCmd>(c)) return 3;     // thin overlays last
+            return 4;
+        };
+        std::stable_sort(m_items.begin(), m_items.end(), [&](const DrawCmd& a, const DrawCmd& b){
+            return weight(a) < weight(b);
+        });
+    }
+private:
+    std::vector<DrawCmd> m_items;
+};
+
+} // namespace Render::GL

+ 14 - 6
render/entity/archer_renderer.cpp

@@ -1,6 +1,5 @@
 #include "archer_renderer.h"
 #include "archer_renderer.h"
 #include "registry.h"
 #include "registry.h"
-#include "../gl/renderer.h"
 #include "../gl/mesh.h"
 #include "../gl/mesh.h"
 #include "../gl/texture.h"
 #include "../gl/texture.h"
 #include "../geom/selection_ring.h"
 #include "../geom/selection_ring.h"
@@ -83,8 +82,7 @@ static Mesh* getArcherCapsule() {
 }
 }
 
 
 void registerArcherRenderer(EntityRendererRegistry& registry) {
 void registerArcherRenderer(EntityRendererRegistry& registry) {
-    registry.registerRenderer("archer", [](const DrawParams& p){
-        if (!p.renderer) return;
+    registry.registerRenderer("archer", [](const DrawContext& p, ISubmitter& out){
         QVector3D color(0.8f, 0.9f, 1.0f);
         QVector3D color(0.8f, 0.9f, 1.0f);
         Engine::Core::UnitComponent* unit = nullptr;
         Engine::Core::UnitComponent* unit = nullptr;
         Engine::Core::RenderableComponent* rc = nullptr;
         Engine::Core::RenderableComponent* rc = nullptr;
@@ -98,15 +96,25 @@ void registerArcherRenderer(EntityRendererRegistry& registry) {
         } else if (rc) {
         } else if (rc) {
             color = QVector3D(rc->color[0], rc->color[1], rc->color[2]);
             color = QVector3D(rc->color[0], rc->color[1], rc->color[2]);
         }
         }
-        // Draw capsule (archer body)
-        p.renderer->drawMeshColored(getArcherCapsule(), p.model, color, nullptr);
+        // Enqueue capsule (archer body)
+        out.mesh(getArcherCapsule(), p.model, color, nullptr, 1.0f);
         // Draw selection ring if selected
         // Draw selection ring if selected
         if (p.selected) {
         if (p.selected) {
             QMatrix4x4 ringM;
             QMatrix4x4 ringM;
             QVector3D pos = p.model.column(3).toVector3D();
             QVector3D pos = p.model.column(3).toVector3D();
             ringM.translate(pos.x(), 0.01f, pos.z());
             ringM.translate(pos.x(), 0.01f, pos.z());
             ringM.scale(0.5f, 1.0f, 0.5f);
             ringM.scale(0.5f, 1.0f, 0.5f);
-            p.renderer->drawMeshColored(Render::Geom::SelectionRing::get(), ringM, QVector3D(0.2f, 0.8f, 0.2f), nullptr);
+            out.selectionRing(ringM, 0.6f, 0.25f, QVector3D(0.2f, 0.8f, 0.2f));
+        }
+
+        // Hover ring (subtle) when hovered but not selected
+        if (p.hovered && !p.selected) {
+            QMatrix4x4 ringM;
+            QVector3D pos = p.model.column(3).toVector3D();
+            ringM.translate(pos.x(), 0.01f, pos.z());
+            ringM.scale(0.5f, 1.0f, 0.5f);
+            // Softer highlight color and alpha than selection
+            out.selectionRing(ringM, 0.35f, 0.15f, QVector3D(0.90f, 0.90f, 0.25f));
         }
         }
     });
     });
 }
 }

+ 2 - 2
render/entity/arrow_vfx_renderer.cpp

@@ -1,6 +1,6 @@
 #include "arrow_vfx_renderer.h"
 #include "arrow_vfx_renderer.h"
 #include "registry.h"
 #include "registry.h"
-#include "../gl/renderer.h"
+#include "../scene_renderer.h"
 #include "../gl/resources.h"
 #include "../gl/resources.h"
 #include "../../game/systems/arrow_system.h"
 #include "../../game/systems/arrow_system.h"
 #include <algorithm>
 #include <algorithm>
@@ -32,7 +32,7 @@ void renderArrows(Renderer* renderer, ResourceManager* resources, const Game::Sy
         const float xyScale = 0.26f;
         const float xyScale = 0.26f;
         model.translate(0.0f, 0.0f, -zScale * 0.5f);
         model.translate(0.0f, 0.0f, -zScale * 0.5f);
         model.scale(xyScale, xyScale, zScale);
         model.scale(xyScale, xyScale, zScale);
-        renderer->drawMeshColored(arrowMesh, model, arrow.color);
+    renderer->mesh(arrowMesh, model, arrow.color, nullptr, 1.0f);
     }
     }
 }
 }
 
 

+ 48 - 21
render/entity/barracks_renderer.cpp

@@ -1,13 +1,12 @@
 #include "barracks_renderer.h"
 #include "barracks_renderer.h"
 #include "registry.h"
 #include "registry.h"
-#include "../gl/renderer.h"
 #include "../gl/resources.h"
 #include "../gl/resources.h"
 #include "../../game/core/component.h"
 #include "../../game/core/component.h"
 
 
 namespace Render::GL {
 namespace Render::GL {
 
 
-static void drawBarracks(const DrawParams& p) {
-    if (!p.renderer || !p.resources || !p.entity) return;
+static void drawBarracks(const DrawContext& p, ISubmitter& out) {
+    if (!p.resources || !p.entity) return;
     auto* t = p.entity->getComponent<Engine::Core::TransformComponent>();
     auto* t = p.entity->getComponent<Engine::Core::TransformComponent>();
     auto* r = p.entity->getComponent<Engine::Core::RenderableComponent>();
     auto* r = p.entity->getComponent<Engine::Core::RenderableComponent>();
     auto* u = p.entity->getComponent<Engine::Core::UnitComponent>();
     auto* u = p.entity->getComponent<Engine::Core::UnitComponent>();
@@ -27,19 +26,19 @@ static void drawBarracks(const DrawParams& p) {
     QMatrix4x4 foundation = p.model;
     QMatrix4x4 foundation = p.model;
     foundation.translate(0.0f, -0.15f, 0.0f);
     foundation.translate(0.0f, -0.15f, 0.0f);
     foundation.scale(1.35f, 0.10f, 1.2f);
     foundation.scale(1.35f, 0.10f, 1.2f);
-    p.renderer->drawMeshColored(p.resources->unit(), foundation, stone, p.resources->white());
+    out.mesh(p.resources->unit(), foundation, stone, p.resources->white(), 1.0f);
 
 
     // Wooden walls (main volume)
     // Wooden walls (main volume)
     QMatrix4x4 walls = p.model;
     QMatrix4x4 walls = p.model;
     walls.scale(1.15f, 0.65f, 0.95f);
     walls.scale(1.15f, 0.65f, 0.95f);
-    p.renderer->drawMeshColored(p.resources->unit(), walls, wood, p.resources->white());
+    out.mesh(p.resources->unit(), walls, wood, p.resources->white(), 1.0f);
 
 
     // Timber beams: vertical corners
     // Timber beams: vertical corners
     auto drawBeam = [&](float x, float z) {
     auto drawBeam = [&](float x, float z) {
         QMatrix4x4 b = p.model;
         QMatrix4x4 b = p.model;
         b.translate(x, 0.20f, z);
         b.translate(x, 0.20f, z);
         b.scale(0.06f, 0.75f, 0.06f);
         b.scale(0.06f, 0.75f, 0.06f);
-        p.renderer->drawMeshColored(p.resources->unit(), b, woodDark, p.resources->white());
+           out.mesh(p.resources->unit(), b, woodDark, p.resources->white(), 1.0f);
     };
     };
     drawBeam( 0.9f,  0.7f);
     drawBeam( 0.9f,  0.7f);
     drawBeam(-0.9f,  0.7f);
     drawBeam(-0.9f,  0.7f);
@@ -50,58 +49,58 @@ static void drawBarracks(const DrawParams& p) {
     QMatrix4x4 roofBase = p.model;
     QMatrix4x4 roofBase = p.model;
     roofBase.translate(0.0f, 0.55f, 0.0f);
     roofBase.translate(0.0f, 0.55f, 0.0f);
     roofBase.scale(1.25f, 0.18f, 1.05f);
     roofBase.scale(1.25f, 0.18f, 1.05f);
-    p.renderer->drawMeshColored(p.resources->unit(), roofBase, thatch, p.resources->white());
+    out.mesh(p.resources->unit(), roofBase, thatch, p.resources->white(), 1.0f);
 
 
     QMatrix4x4 roofMid = p.model;
     QMatrix4x4 roofMid = p.model;
     roofMid.translate(0.0f, 0.72f, 0.0f);
     roofMid.translate(0.0f, 0.72f, 0.0f);
     roofMid.scale(1.05f, 0.14f, 0.95f);
     roofMid.scale(1.05f, 0.14f, 0.95f);
-    p.renderer->drawMeshColored(p.resources->unit(), roofMid, thatch, p.resources->white());
+    out.mesh(p.resources->unit(), roofMid, thatch, p.resources->white(), 1.0f);
 
 
     QMatrix4x4 roofRidge = p.model;
     QMatrix4x4 roofRidge = p.model;
     roofRidge.translate(0.0f, 0.86f, 0.0f);
     roofRidge.translate(0.0f, 0.86f, 0.0f);
     roofRidge.scale(0.85f, 0.12f, 0.85f);
     roofRidge.scale(0.85f, 0.12f, 0.85f);
-    p.renderer->drawMeshColored(p.resources->unit(), roofRidge, thatch, p.resources->white());
+    out.mesh(p.resources->unit(), roofRidge, thatch, p.resources->white(), 1.0f);
 
 
     // Roof ridge beam
     // Roof ridge beam
     QMatrix4x4 ridge = p.model;
     QMatrix4x4 ridge = p.model;
     ridge.translate(0.0f, 0.96f, 0.0f);
     ridge.translate(0.0f, 0.96f, 0.0f);
     ridge.scale(1.1f, 0.04f, 0.12f);
     ridge.scale(1.1f, 0.04f, 0.12f);
-    p.renderer->drawMeshColored(p.resources->unit(), ridge, woodDark, p.resources->white());
+    out.mesh(p.resources->unit(), ridge, woodDark, p.resources->white(), 1.0f);
 
 
     // Door at front
     // Door at front
     QMatrix4x4 door = p.model;
     QMatrix4x4 door = p.model;
     door.translate(0.0f, -0.02f, 0.62f);
     door.translate(0.0f, -0.02f, 0.62f);
     door.scale(0.25f, 0.38f, 0.06f);
     door.scale(0.25f, 0.38f, 0.06f);
-    p.renderer->drawMeshColored(p.resources->unit(), door, doorColor, p.resources->white());
+    out.mesh(p.resources->unit(), door, doorColor, p.resources->white(), 1.0f);
 
 
     // Side annex (shed) on the right
     // Side annex (shed) on the right
     QMatrix4x4 annex = p.model;
     QMatrix4x4 annex = p.model;
     annex.translate(0.95f, -0.05f, -0.15f);
     annex.translate(0.95f, -0.05f, -0.15f);
     annex.scale(0.55f, 0.45f, 0.55f);
     annex.scale(0.55f, 0.45f, 0.55f);
-    p.renderer->drawMeshColored(p.resources->unit(), annex, wood, p.resources->white());
+    out.mesh(p.resources->unit(), annex, wood, p.resources->white(), 1.0f);
 
 
     QMatrix4x4 annexRoof = p.model;
     QMatrix4x4 annexRoof = p.model;
     annexRoof.translate(0.95f, 0.30f, -0.15f);
     annexRoof.translate(0.95f, 0.30f, -0.15f);
     annexRoof.scale(0.60f, 0.12f, 0.60f);
     annexRoof.scale(0.60f, 0.12f, 0.60f);
-    p.renderer->drawMeshColored(p.resources->unit(), annexRoof, thatch, p.resources->white());
+    out.mesh(p.resources->unit(), annexRoof, thatch, p.resources->white(), 1.0f);
 
 
     // Chimney on the back-left
     // Chimney on the back-left
     QMatrix4x4 chimney = p.model;
     QMatrix4x4 chimney = p.model;
     chimney.translate(-0.65f, 0.75f, -0.55f);
     chimney.translate(-0.65f, 0.75f, -0.55f);
     chimney.scale(0.10f, 0.35f, 0.10f);
     chimney.scale(0.10f, 0.35f, 0.10f);
-    p.renderer->drawMeshColored(p.resources->unit(), chimney, stone, p.resources->white());
+    out.mesh(p.resources->unit(), chimney, stone, p.resources->white(), 1.0f);
 
 
     QMatrix4x4 chimneyCap = p.model;
     QMatrix4x4 chimneyCap = p.model;
     chimneyCap.translate(-0.65f, 0.95f, -0.55f);
     chimneyCap.translate(-0.65f, 0.95f, -0.55f);
     chimneyCap.scale(0.16f, 0.05f, 0.16f);
     chimneyCap.scale(0.16f, 0.05f, 0.16f);
-    p.renderer->drawMeshColored(p.resources->unit(), chimneyCap, stone, p.resources->white());
+    out.mesh(p.resources->unit(), chimneyCap, stone, p.resources->white(), 1.0f);
 
 
     // Path stones leading from door
     // Path stones leading from door
     auto drawPaver = [&](float ox, float oz, float sx, float sz) {
     auto drawPaver = [&](float ox, float oz, float sx, float sz) {
         QMatrix4x4 paver = p.model;
         QMatrix4x4 paver = p.model;
         paver.translate(ox, -0.14f, oz);
         paver.translate(ox, -0.14f, oz);
         paver.scale(sx, 0.02f, sz);
         paver.scale(sx, 0.02f, sz);
-        p.renderer->drawMeshColored(p.resources->unit(), paver, path, p.resources->white());
+    out.mesh(p.resources->unit(), paver, path, p.resources->white(), 1.0f);
     };
     };
     drawPaver( 0.0f,  0.9f, 0.25f, 0.20f);
     drawPaver( 0.0f,  0.9f, 0.25f, 0.20f);
     drawPaver( 0.0f,  1.15f, 0.22f, 0.18f);
     drawPaver( 0.0f,  1.15f, 0.22f, 0.18f);
@@ -109,28 +108,56 @@ static void drawBarracks(const DrawParams& p) {
 
 
     // Crates near the door
     // Crates near the door
     QMatrix4x4 crate1 = p.model; crate1.translate(0.45f, -0.05f, 0.55f); crate1.scale(0.18f, 0.18f, 0.18f);
     QMatrix4x4 crate1 = p.model; crate1.translate(0.45f, -0.05f, 0.55f); crate1.scale(0.18f, 0.18f, 0.18f);
-    p.renderer->drawMeshColored(p.resources->unit(), crate1, crate, p.resources->white());
+    out.mesh(p.resources->unit(), crate1, crate, p.resources->white(), 1.0f);
     QMatrix4x4 crate2 = p.model; crate2.translate(0.58f, 0.02f, 0.45f); crate2.scale(0.14f, 0.14f, 0.14f);
     QMatrix4x4 crate2 = p.model; crate2.translate(0.58f, 0.02f, 0.45f); crate2.scale(0.14f, 0.14f, 0.14f);
-    p.renderer->drawMeshColored(p.resources->unit(), crate2, crate, p.resources->white());
+    out.mesh(p.resources->unit(), crate2, crate, p.resources->white(), 1.0f);
 
 
     // Simple fence posts on front-left
     // Simple fence posts on front-left
     for (int i=0;i<3;++i) {
     for (int i=0;i<3;++i) {
         QMatrix4x4 post = p.model;
         QMatrix4x4 post = p.model;
         post.translate(-0.85f + 0.18f * i, -0.05f, 0.85f);
         post.translate(-0.85f + 0.18f * i, -0.05f, 0.85f);
         post.scale(0.05f, 0.25f, 0.05f);
         post.scale(0.05f, 0.25f, 0.05f);
-        p.renderer->drawMeshColored(p.resources->unit(), post, woodDark, p.resources->white());
+    out.mesh(p.resources->unit(), post, woodDark, p.resources->white(), 1.0f);
     }
     }
 
 
     // Banner pole with team-colored flag
     // Banner pole with team-colored flag
     QMatrix4x4 pole = p.model;
     QMatrix4x4 pole = p.model;
     pole.translate(-0.9f, 0.55f, -0.55f);
     pole.translate(-0.9f, 0.55f, -0.55f);
     pole.scale(0.05f, 0.7f, 0.05f);
     pole.scale(0.05f, 0.7f, 0.05f);
-    p.renderer->drawMeshColored(p.resources->unit(), pole, woodDark, p.resources->white());
+    out.mesh(p.resources->unit(), pole, woodDark, p.resources->white(), 1.0f);
 
 
     QMatrix4x4 banner = p.model;
     QMatrix4x4 banner = p.model;
     banner.translate(-0.82f, 0.80f, -0.50f);
     banner.translate(-0.82f, 0.80f, -0.50f);
     banner.scale(0.35f, 0.22f, 0.02f);
     banner.scale(0.35f, 0.22f, 0.02f);
-    p.renderer->drawMeshColored(p.resources->unit(), banner, team, p.resources->white());
+    out.mesh(p.resources->unit(), banner, team, p.resources->white(), 1.0f);
+
+    // Rally flag if set
+    if (auto* prod = p.entity->getComponent<Engine::Core::ProductionComponent>()) {
+        if (prod->rallySet && p.resources) {
+            QMatrix4x4 flagModel;
+            flagModel.translate(prod->rallyX, 0.1f, prod->rallyZ);
+            flagModel.scale(0.2f, 0.2f, 0.2f);
+            out.mesh(p.resources->unit(), flagModel, QVector3D(1.0f, 0.9f, 0.2f), p.resources->white(), 1.0f);
+        }
+    }
+
+    // Selection smoke if selected (twice the previous diameter ~ scale *2.0)
+    if (p.selected) {
+        QMatrix4x4 m;
+        QVector3D pos = p.model.column(3).toVector3D();
+        // Force ground-level placement; ignore model's Y height for discs
+        m.translate(pos.x(), 0.0f, pos.z());
+        m.scale(1.8f, 1.0f, 1.8f); // previously ~0.9, now doubled
+    out.selectionSmoke(m, QVector3D(0.2f, 0.8f, 0.2f), 0.32f);
+    }
+    // Hover smoke if hovered and not selected (subtle)
+    else if (p.hovered) {
+        QMatrix4x4 m;
+        QVector3D pos = p.model.column(3).toVector3D();
+        m.translate(pos.x(), 0.0f, pos.z());
+        m.scale(1.8f, 1.0f, 1.8f);
+    out.selectionSmoke(m, QVector3D(0.90f, 0.90f, 0.25f), 0.20f);
+    }
 }
 }
 
 
 void registerBarracksRenderer(EntityRendererRegistry& registry) {
 void registerBarracksRenderer(EntityRendererRegistry& registry) {

+ 1 - 1
render/entity/registry.cpp

@@ -1,7 +1,7 @@
 #include "registry.h"
 #include "registry.h"
 #include "archer_renderer.h"
 #include "archer_renderer.h"
 #include "barracks_renderer.h"
 #include "barracks_renderer.h"
-#include "../gl/renderer.h"
+#include "../scene_renderer.h"
 #include "../../game/core/entity.h"
 #include "../../game/core/entity.h"
 #include "../../game/core/component.h"
 #include "../../game/core/component.h"
 
 

+ 5 - 4
render/entity/registry.h

@@ -6,21 +6,22 @@
 #include <unordered_map>
 #include <unordered_map>
 #include <QMatrix4x4>
 #include <QMatrix4x4>
 #include <QVector3D>
 #include <QVector3D>
+#include "../submitter.h"
 
 
 namespace Engine { namespace Core { class Entity; } }
 namespace Engine { namespace Core { class Entity; } }
-namespace Render { namespace GL { class Renderer; class ResourceManager; class Mesh; class Texture; } }
+namespace Render { namespace GL { class ResourceManager; class Mesh; class Texture; } }
 
 
 namespace Render::GL {
 namespace Render::GL {
 
 
-struct DrawParams {
-    Renderer* renderer = nullptr;
+struct DrawContext {
     ResourceManager* resources = nullptr;
     ResourceManager* resources = nullptr;
     Engine::Core::Entity* entity = nullptr;
     Engine::Core::Entity* entity = nullptr;
     QMatrix4x4 model;
     QMatrix4x4 model;
     bool selected = false;
     bool selected = false;
+    bool hovered = false;
 };
 };
 
 
-using RenderFunc = std::function<void(const DrawParams&)>;
+using RenderFunc = std::function<void(const DrawContext&, ISubmitter& out)>;
 
 
 class EntityRendererRegistry {
 class EntityRendererRegistry {
 public:
 public:

+ 2 - 2
render/geom/arrow.cpp

@@ -1,7 +1,7 @@
 #include "arrow.h"
 #include "arrow.h"
 #include "../gl/mesh.h"
 #include "../gl/mesh.h"
 #include "../entity/registry.h"
 #include "../entity/registry.h"
-#include "../gl/renderer.h"
+#include "../scene_renderer.h"
 #include "../gl/resources.h"
 #include "../gl/resources.h"
 #include "../../game/systems/arrow_system.h"
 #include "../../game/systems/arrow_system.h"
 
 
@@ -118,7 +118,7 @@ void renderArrows(Renderer* renderer,
         model.translate(0.0f, 0.0f, -ARROW_Z_SCALE * ARROW_Z_TRANSLATE_FACTOR);
         model.translate(0.0f, 0.0f, -ARROW_Z_SCALE * ARROW_Z_TRANSLATE_FACTOR);
         model.scale(ARROW_XY_SCALE, ARROW_XY_SCALE, ARROW_Z_SCALE);
         model.scale(ARROW_XY_SCALE, ARROW_XY_SCALE, ARROW_Z_SCALE);
 
 
-        renderer->drawMeshColored(arrowMesh, model, arrow.color);
+    renderer->mesh(arrowMesh, model, arrow.color, nullptr, 1.0f);
     }
     }
 }
 }
 
 

+ 36 - 0
render/geom/selection_disc.cpp

@@ -0,0 +1,36 @@
+#include "selection_disc.h"
+#include <QVector3D>
+#include <vector>
+#include <cmath>
+
+namespace Render::Geom {
+
+std::unique_ptr<Render::GL::Mesh> SelectionDisc::s_mesh;
+
+static Render::GL::Mesh* createDiscMesh() {
+    using namespace Render::GL;
+    std::vector<Vertex> verts;
+    std::vector<unsigned int> idx;
+    const int seg = 72;
+    // Center vertex
+    verts.push_back({{0.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.5f, 0.5f}});
+    for (int i = 0; i <= seg; ++i) {
+        float a = (i / float(seg)) * 6.2831853f;
+        float x = std::cos(a);
+        float z = std::sin(a);
+        verts.push_back({{x, 0.0f, z}, {0.0f, 1.0f, 0.0f}, {0.5f + 0.5f*x, 0.5f + 0.5f*z}});
+    }
+    for (int i = 1; i <= seg; ++i) {
+        idx.push_back(0);
+        idx.push_back(i);
+        idx.push_back(i+1);
+    }
+    return new Mesh(verts, idx);
+}
+
+Render::GL::Mesh* SelectionDisc::get() {
+    if (!s_mesh) s_mesh.reset(createDiscMesh());
+    return s_mesh.get();
+}
+
+} // namespace Render::Geom

+ 15 - 0
render/geom/selection_disc.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include "../gl/mesh.h"
+#include <memory>
+
+namespace Render::Geom {
+
+class SelectionDisc {
+public:
+    static Render::GL::Mesh* get();
+private:
+    static std::unique_ptr<Render::GL::Mesh> s_mesh;
+};
+
+} // namespace Render::Geom

+ 156 - 0
render/gl/backend.cpp

@@ -0,0 +1,156 @@
+#include "backend.h"
+#include "shader.h"
+#include "../draw_queue.h"
+#include "mesh.h"
+#include "texture.h"
+#include "../geom/selection_ring.h"
+#include "../geom/selection_disc.h"
+#include "state_scopes.h"
+#include <QDebug>
+
+namespace Render::GL {
+Backend::~Backend() = default;
+
+void Backend::initialize() {
+	initializeOpenGLFunctions();
+	glEnable(GL_DEPTH_TEST);
+	glDepthFunc(GL_LESS);
+	glEnable(GL_BLEND);
+	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+	// Create resource and shader managers
+	m_resources = std::make_unique<ResourceManager>();
+	if (!m_resources->initialize()) {
+		qWarning() << "Backend: failed to initialize ResourceManager";
+	}
+	m_shaderCache = std::make_unique<ShaderCache>();
+	m_shaderCache->initializeDefaults();
+	m_basicShader = m_shaderCache->get(QStringLiteral("basic"));
+	m_gridShader  = m_shaderCache->get(QStringLiteral("grid"));
+	if (!m_basicShader) qWarning() << "Backend: basic shader missing";
+	if (!m_gridShader)  qWarning() << "Backend: grid shader missing";
+}
+
+void Backend::beginFrame() {
+	if (m_viewportWidth > 0 && m_viewportHeight > 0) {
+		glViewport(0, 0, m_viewportWidth, m_viewportHeight);
+	}
+	glClearColor(m_clearColor[0], m_clearColor[1], m_clearColor[2], m_clearColor[3]);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+}
+
+void Backend::setViewport(int w, int h) {
+	m_viewportWidth = w;
+	m_viewportHeight = h;
+}
+
+void Backend::setClearColor(float r, float g, float b, float a) {
+	m_clearColor[0]=r; m_clearColor[1]=g; m_clearColor[2]=b; m_clearColor[3]=a;
+}
+
+void Backend::execute(const DrawQueue& queue, const Camera& cam) {
+	if (!m_basicShader) return;
+	// Bind once up front; we'll defensively rebind before draws that use it
+	m_basicShader->use();
+	m_basicShader->setUniform("u_view", cam.getViewMatrix());
+	m_basicShader->setUniform("u_projection", cam.getProjectionMatrix());
+	for (const auto& cmd : queue.items()) {
+		if (std::holds_alternative<MeshCmd>(cmd)) {
+			const auto& it = std::get<MeshCmd>(cmd);
+			if (!it.mesh) continue;
+			// Reset critical state for opaque mesh pass
+			glDepthMask(GL_TRUE);
+			if (glIsEnabled(GL_POLYGON_OFFSET_FILL)) glDisable(GL_POLYGON_OFFSET_FILL);
+			// Ensure the correct program is bound in case a prior GridCmd changed it
+			m_basicShader->use();
+			m_basicShader->setUniform("u_view", cam.getViewMatrix());
+			m_basicShader->setUniform("u_projection", cam.getProjectionMatrix());
+			m_basicShader->setUniform("u_model", it.model);
+			if (it.texture) {
+				it.texture->bind(0);
+				m_basicShader->setUniform("u_texture", 0);
+				m_basicShader->setUniform("u_useTexture", true);
+			} else {
+				if (m_resources && m_resources->white()) {
+					m_resources->white()->bind(0);
+					m_basicShader->setUniform("u_texture", 0);
+				}
+				m_basicShader->setUniform("u_useTexture", false);
+			}
+			m_basicShader->setUniform("u_color", it.color);
+			m_basicShader->setUniform("u_alpha", it.alpha);
+			it.mesh->draw();
+		} else if (std::holds_alternative<GridCmd>(cmd)) {
+			if (!m_gridShader) continue;
+			const auto& gc = std::get<GridCmd>(cmd);
+			m_gridShader->use();
+			m_gridShader->setUniform("u_view", cam.getViewMatrix());
+			m_gridShader->setUniform("u_projection", cam.getProjectionMatrix());
+			// Model is already scaled appropriately; do not apply extent again
+			QMatrix4x4 model = gc.model;
+			// model.scale(gc.extent, 1.0f, gc.extent); // Removed redundant scaling
+			m_gridShader->setUniform("u_model", model);
+			m_gridShader->setUniform("u_gridColor", gc.color);
+			m_gridShader->setUniform("u_lineColor", QVector3D(0.22f, 0.25f, 0.22f));
+			m_gridShader->setUniform("u_cellSize", gc.cellSize);
+			m_gridShader->setUniform("u_thickness", gc.thickness);
+			// Draw a full plane using the default ground mesh if available
+			if (m_resources) {
+				if (auto* plane = m_resources->ground()) plane->draw();
+			}
+			// Do not release to program 0 here; subsequent draws will rebind their shader as needed
+		} else if (std::holds_alternative<SelectionRingCmd>(cmd)) {
+			const auto& sc = std::get<SelectionRingCmd>(cmd);
+			Mesh* ring = Render::Geom::SelectionRing::get();
+			if (!ring) continue;
+			// Ensure basic program is bound
+			m_basicShader->use();
+			m_basicShader->setUniform("u_view", cam.getViewMatrix());
+			m_basicShader->setUniform("u_projection", cam.getProjectionMatrix());
+			// Use white texture path for consistent shading
+			m_basicShader->setUniform("u_useTexture", false);
+			m_basicShader->setUniform("u_color", sc.color);
+			// Slight polygon offset and disable depth writes while blending
+			DepthMaskScope depthMask(false);
+			PolygonOffsetScope poly(-1.0f, -1.0f);
+			BlendScope blend(true);
+			// Outer feather
+			{
+				QMatrix4x4 m = sc.model; m.scale(1.08f, 1.0f, 1.08f);
+				m_basicShader->setUniform("u_model", m);
+				m_basicShader->setUniform("u_alpha", sc.alphaOuter);
+				ring->draw();
+			}
+			// Inner ring
+			{
+				m_basicShader->setUniform("u_model", sc.model);
+				m_basicShader->setUniform("u_alpha", sc.alphaInner);
+				ring->draw();
+			}
+		} else if (std::holds_alternative<SelectionSmokeCmd>(cmd)) {
+			const auto& sm = std::get<SelectionSmokeCmd>(cmd);
+			Mesh* disc = Render::Geom::SelectionDisc::get();
+			if (!disc) continue;
+			m_basicShader->use();
+			m_basicShader->setUniform("u_view", cam.getViewMatrix());
+			m_basicShader->setUniform("u_projection", cam.getProjectionMatrix());
+			m_basicShader->setUniform("u_useTexture", false);
+			m_basicShader->setUniform("u_color", sm.color);
+			DepthMaskScope depthMask(false);
+			DepthTestScope depthTest(false); // draw without depth test so smoke sits visible over ground
+			// Slight negative offset helps avoid coincident z with ground if depth test toggles elsewhere
+			PolygonOffsetScope poly(-0.1f, -0.1f);
+			BlendScope blend(true);
+			for (int i = 0; i < 7; ++i) {
+				float scale = 1.35f + 0.12f * i;
+				float a = sm.baseAlpha * (1.0f - 0.09f * i);
+				QMatrix4x4 m = sm.model; m.translate(0.0f, 0.02f, 0.0f); m.scale(scale, 1.0f, scale);
+				m_basicShader->setUniform("u_model", m);
+				m_basicShader->setUniform("u_alpha", a);
+				disc->draw();
+			}
+		}
+	}
+	m_basicShader->release();
+}
+
+} // namespace Render::GL

+ 61 - 0
render/gl/backend.h

@@ -0,0 +1,61 @@
+#pragma once
+
+#include <QOpenGLFunctions_3_3_Core>
+#include "../draw_queue.h"
+#include "camera.h"
+#include "resources.h"
+#include <array>
+#include <memory>
+#include "shader.h"
+#include "shader_cache.h"
+
+namespace Render::GL {
+// Shader included above
+
+class Backend : protected QOpenGLFunctions_3_3_Core {
+public:
+    Backend() = default;
+    ~Backend();
+    void initialize();
+    void beginFrame();
+    void setViewport(int w, int h);
+    void setClearColor(float r, float g, float b, float a);
+    void execute(const DrawQueue& queue, const Camera& cam);
+
+    // Access to GL resources for high-level systems that need mesh pointers
+    ResourceManager* resources() const { return m_resources.get(); }
+
+    // Lazy shader access/loading for optional, entity-specific effects
+    Shader* shader(const QString& name) const { return m_shaderCache ? m_shaderCache->get(name) : nullptr; }
+    Shader* getOrLoadShader(const QString& name, const QString& vertPath, const QString& fragPath) {
+        if (!m_shaderCache) return nullptr;
+        return m_shaderCache->load(name, vertPath, fragPath);
+    }
+
+    void enableDepthTest(bool enable) {
+        if (enable) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
+    }
+    void setDepthFunc(GLenum func) { glDepthFunc(func); }
+    void setDepthMask(bool write) { glDepthMask(write ? GL_TRUE : GL_FALSE); }
+
+    void enableBlend(bool enable) {
+        if (enable) glEnable(GL_BLEND); else glDisable(GL_BLEND);
+    }
+    void setBlendFunc(GLenum src, GLenum dst) { glBlendFunc(src, dst); }
+
+    void enablePolygonOffset(bool enable) {
+        if (enable) glEnable(GL_POLYGON_OFFSET_FILL); else glDisable(GL_POLYGON_OFFSET_FILL);
+    }
+    void setPolygonOffset(float factor, float units) { glPolygonOffset(factor, units); }
+private:
+    int m_viewportWidth{0};
+    int m_viewportHeight{0};
+    std::array<float,4> m_clearColor{0.2f,0.3f,0.3f,0.0f};
+    std::unique_ptr<ShaderCache> m_shaderCache;
+    std::unique_ptr<ResourceManager> m_resources;
+    // Cached pointers to named shaders (owned by m_shaderCache)
+    Shader* m_basicShader = nullptr;
+    Shader* m_gridShader = nullptr;
+};
+
+} // namespace Render::GL

+ 2 - 5
render/gl/bootstrap.cpp

@@ -1,5 +1,5 @@
 #include "bootstrap.h"
 #include "bootstrap.h"
-#include "renderer.h"
+#include "../scene_renderer.h"
 #include "camera.h"
 #include "camera.h"
 #include "resources.h"
 #include "resources.h"
 #include <QOpenGLContext>
 #include <QOpenGLContext>
@@ -8,15 +8,12 @@
 namespace Render { namespace GL {
 namespace Render { namespace GL {
 
 
 bool RenderBootstrap::initialize(Renderer& renderer,
 bool RenderBootstrap::initialize(Renderer& renderer,
-                                 Camera& camera,
-                                 std::shared_ptr<ResourceManager>& outResources) {
+                                 Camera& camera) {
     QOpenGLContext* ctx = QOpenGLContext::currentContext();
     QOpenGLContext* ctx = QOpenGLContext::currentContext();
     if (!ctx || !ctx->isValid()) {
     if (!ctx || !ctx->isValid()) {
         qWarning() << "RenderBootstrap: no current valid OpenGL context";
         qWarning() << "RenderBootstrap: no current valid OpenGL context";
         return false;
         return false;
     }
     }
-    outResources = std::make_shared<ResourceManager>();
-    renderer.setResources(outResources);
     if (!renderer.initialize()) {
     if (!renderer.initialize()) {
         qWarning() << "RenderBootstrap: renderer initialize failed";
         qWarning() << "RenderBootstrap: renderer initialize failed";
         return false;
         return false;

+ 2 - 3
render/gl/bootstrap.h

@@ -11,10 +11,9 @@ class ResourceManager;
 
 
 class RenderBootstrap {
 class RenderBootstrap {
 public:
 public:
-    // Initializes GL renderer with resources and binds camera. Returns false if no valid GL context or renderer init fails.
+    // Initializes GL renderer and binds camera. Returns false if no valid GL context or renderer init fails.
     static bool initialize(Renderer& renderer,
     static bool initialize(Renderer& renderer,
-                           Camera& camera,
-                           std::shared_ptr<ResourceManager>& outResources);
+                           Camera& camera);
 };
 };
 
 
 } } // namespace Render::GL
 } } // namespace Render::GL

+ 0 - 367
render/gl/renderer.cpp

@@ -1,367 +0,0 @@
-#include "renderer.h"
-#include "../../game/core/world.h"
-#include "../../game/core/component.h"
-#include <QDebug>
-#include <QOpenGLContext>
-#include <algorithm>
-#include <cmath>
-#include "../entity/registry.h"
-#include "../geom/selection_ring.h"
-
-namespace Render::GL {
-
-Renderer::Renderer() {
-    // Defer OpenGL function initialization until a valid context is current
-}
-
-Renderer::~Renderer() {
-    shutdown();
-}
-
-bool Renderer::initialize() {
-    // Ensure an OpenGL context is current before using any GL calls
-    if (!QOpenGLContext::currentContext()) {
-        qWarning() << "Renderer::initialize called without a current OpenGL context";
-        return false;
-    }
-
-    initializeOpenGLFunctions();
-    // Enable depth testing
-    glEnable(GL_DEPTH_TEST);
-    glDepthFunc(GL_LESS);
-    
-    // Enable blending for transparency
-    glEnable(GL_BLEND);
-    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-    
-    // Set default clear color with alpha 0 to allow QML overlay compositing
-    setClearColor(0.2f, 0.3f, 0.3f, 0.0f);
-    
-    if (!loadShaders()) {
-        return false;
-    }
-    // Initialize shared/default GL resources when not injected
-    if (!m_resources) m_resources = std::make_shared<ResourceManager>();
-    if (!m_resources->initialize()) {
-        qWarning() << "Failed to initialize GL resources";
-        // Non-fatal: renderer can still function in a limited capacity
-    }
-    // Set up entity renderer registry (built-ins)
-    m_entityRegistry = std::make_unique<EntityRendererRegistry>();
-    registerBuiltInEntityRenderers(*m_entityRegistry);
-    
-    return true;
-}
-
-void Renderer::shutdown() {
-    m_basicShader.reset();
-    m_lineShader.reset();
-    m_gridShader.reset();
-    m_resources.reset();
-}
-
-void Renderer::beginFrame() {
-    if (m_viewportWidth > 0 && m_viewportHeight > 0) {
-        glViewport(0, 0, m_viewportWidth, m_viewportHeight);
-    }
-    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-    m_renderQueue.clear();
-}
-
-void Renderer::endFrame() {
-    flushBatch();
-}
-void Renderer::renderGridGround() {
-    Mesh* groundMesh = (m_resources ? m_resources->ground() : nullptr);
-    if (!groundMesh || !m_camera) return;
-    QMatrix4x4 groundModel;
-    groundModel.translate(0.0f, 0.0f, 0.0f);
-    groundModel.scale(m_gridParams.extent, 1.0f, m_gridParams.extent);
-    if (m_gridShader) {
-        m_gridShader->use();
-        m_gridShader->setUniform("u_model", groundModel);
-        m_gridShader->setUniform("u_view", m_camera->getViewMatrix());
-        m_gridShader->setUniform("u_projection", m_camera->getProjectionMatrix());
-        m_gridShader->setUniform("u_gridColor", m_gridParams.gridColor);
-        m_gridShader->setUniform("u_lineColor", m_gridParams.lineColor);
-        m_gridShader->setUniform("u_cellSize", m_gridParams.cellSize);
-        m_gridShader->setUniform("u_thickness", m_gridParams.thickness);
-        groundMesh->draw();
-        m_gridShader->release();
-    } else {
-        drawMeshColored(groundMesh, groundModel, m_gridParams.gridColor);
-    }
-}
-
-void Renderer::setCamera(Camera* camera) {
-    m_camera = camera;
-}
-
-void Renderer::setClearColor(float r, float g, float b, float a) {
-    glClearColor(r, g, b, a);
-}
-
-void Renderer::setViewport(int width, int height) {
-    m_viewportWidth = width;
-    m_viewportHeight = height;
-    if (m_camera && height > 0) {
-        float aspect = float(width) / float(height);
-        m_camera->setPerspective(m_camera->getFOV(), aspect, m_camera->getNear(), m_camera->getFar());
-    }
-}
-
-void Renderer::drawMesh(Mesh* mesh, const QMatrix4x4& modelMatrix, Texture* texture) {
-    if (!mesh || !m_basicShader || !m_camera) {
-        return;
-    }
-    
-    m_basicShader->use();
-    
-    // Set matrices
-    m_basicShader->setUniform("u_model", modelMatrix);
-    m_basicShader->setUniform("u_view", m_camera->getViewMatrix());
-    m_basicShader->setUniform("u_projection", m_camera->getProjectionMatrix());
-    m_basicShader->setUniform("u_alpha", 1.0f);
-    
-    // Bind texture
-    if (texture) {
-        texture->bind(0);
-        m_basicShader->setUniform("u_texture", 0);
-        m_basicShader->setUniform("u_useTexture", true);
-    } else {
-        if (m_resources && m_resources->white()) {
-            m_resources->white()->bind(0);
-            m_basicShader->setUniform("u_texture", 0);
-        }
-        m_basicShader->setUniform("u_useTexture", false);
-    }
-    
-    m_basicShader->setUniform("u_color", QVector3D(1.0f, 1.0f, 1.0f));
-    
-    mesh->draw();
-    
-    m_basicShader->release();
-}
-
-void Renderer::drawMeshColored(Mesh* mesh, const QMatrix4x4& modelMatrix, const QVector3D& color, Texture* texture) {
-    if (!mesh || !m_basicShader || !m_camera) {
-        return;
-    }
-    m_basicShader->use();
-    m_basicShader->setUniform("u_model", modelMatrix);
-    m_basicShader->setUniform("u_view", m_camera->getViewMatrix());
-    m_basicShader->setUniform("u_projection", m_camera->getProjectionMatrix());
-    // Alpha default 1, caller can adjust via another shader call if needed
-    m_basicShader->setUniform("u_alpha", 1.0f);
-    if (texture) {
-        texture->bind(0);
-        m_basicShader->setUniform("u_texture", 0);
-        m_basicShader->setUniform("u_useTexture", true);
-    } else {
-        if (m_resources && m_resources->white()) {
-            m_resources->white()->bind(0);
-            m_basicShader->setUniform("u_texture", 0);
-        }
-        m_basicShader->setUniform("u_useTexture", false);
-    }
-    m_basicShader->setUniform("u_color", color);
-    mesh->draw();
-    m_basicShader->release();
-}
-
-void Renderer::drawLine(const QVector3D& start, const QVector3D& end, const QVector3D& color) {
-    // Simple line drawing implementation
-    // In a full implementation, you'd want a proper line renderer
-}
-
-void Renderer::submitRenderCommand(const RenderCommand& command) {
-    m_renderQueue.push_back(command);
-}
-
-void Renderer::flushBatch() {
-    if (m_renderQueue.empty()) {
-        return;
-    }
-    
-    sortRenderQueue();
-    
-    for (const auto& command : m_renderQueue) {
-        drawMeshColored(command.mesh, command.modelMatrix, command.color, command.texture);
-    }
-    
-    m_renderQueue.clear();
-}
-
-void Renderer::renderWorld(Engine::Core::World* world) {
-    if (!world) {
-        return;
-    }
-    // Draw ground plane with grid using helper
-    renderGridGround();
-
-    // Draw hover ring before entities so buildings naturally occlude it
-    if (m_hoveredBuildingId) {
-        if (auto* hovered = world->getEntity(m_hoveredBuildingId)) {
-            if (hovered->hasComponent<Engine::Core::BuildingComponent>()) {
-                if (auto* t = hovered->getComponent<Engine::Core::TransformComponent>()) {
-                    Mesh* ring = Render::Geom::SelectionRing::get();
-                    if (ring && m_basicShader && m_camera) {
-                        const float marginXZ = 1.25f;
-                        const float pad = 1.06f;
-                        float sx = std::max(0.6f, t->scale.x * marginXZ * pad * 1.5f);
-                        float sz = std::max(0.6f, t->scale.z * marginXZ * pad * 1.5f);
-                        QMatrix4x4 model;
-                        model.translate(t->position.x, 0.01f, t->position.z);
-                        model.scale(sx, 1.0f, sz);
-                        // Shadow-like color (dark gray)
-                        QVector3D c(0.0f, 0.0f, 0.0f);
-                        // Slightly bias depth to the ground and disable depth writes so later geometry can overwrite
-                        glEnable(GL_POLYGON_OFFSET_FILL);
-                        glPolygonOffset(1.0f, 1.0f);
-                        // Disable depth writes so later geometry can overwrite the ring
-                        glDepthMask(GL_FALSE);
-                        m_basicShader->use();
-                        m_basicShader->setUniform("u_model", model);
-                        m_basicShader->setUniform("u_view", m_camera->getViewMatrix());
-                        m_basicShader->setUniform("u_projection", m_camera->getProjectionMatrix());
-                        if (m_resources && m_resources->white()) {
-                            m_resources->white()->bind(0);
-                            m_basicShader->setUniform("u_texture", 0);
-                        }
-                        m_basicShader->setUniform("u_useTexture", false);
-                        m_basicShader->setUniform("u_color", c);
-                        // Soft shadow edge
-                        m_basicShader->setUniform("u_alpha", 0.10f);
-                        QMatrix4x4 feather = model; feather.scale(1.08f, 1.0f, 1.08f);
-                        m_basicShader->setUniform("u_model", feather);
-                        ring->draw();
-                        // Main shadow ring
-                        m_basicShader->setUniform("u_model", model);
-                        m_basicShader->setUniform("u_alpha", 0.28f);
-                        ring->draw();
-                        m_basicShader->release();
-                        glDepthMask(GL_TRUE);
-                        glDisable(GL_POLYGON_OFFSET_FILL);
-                    }
-                }
-            }
-        }
-    }
-
-    // Get all entities with both transform and renderable components
-    auto renderableEntities = world->getEntitiesWith<Engine::Core::RenderableComponent>();
-
-    for (auto entity : renderableEntities) {
-        auto renderable = entity->getComponent<Engine::Core::RenderableComponent>();
-        auto transform = entity->getComponent<Engine::Core::TransformComponent>();
-
-        if (!renderable->visible || !transform) {
-            continue;
-        }
-
-        // Build model matrix from transform
-        QMatrix4x4 modelMatrix;
-        modelMatrix.translate(transform->position.x, transform->position.y, transform->position.z);
-        modelMatrix.rotate(transform->rotation.x, QVector3D(1, 0, 0));
-        modelMatrix.rotate(transform->rotation.y, QVector3D(0, 1, 0));
-        modelMatrix.rotate(transform->rotation.z, QVector3D(0, 0, 1));
-        modelMatrix.scale(transform->scale.x, transform->scale.y, transform->scale.z);
-
-        // If entity has a unitType, try registry-based renderer first
-        bool drawnByRegistry = false;
-        if (auto* unit = entity->getComponent<Engine::Core::UnitComponent>()) {
-            if (!unit->unitType.empty() && m_entityRegistry) {
-                auto fn = m_entityRegistry->get(unit->unitType);
-                if (fn) {
-                    DrawParams params{this, m_resources.get(), entity, modelMatrix};
-                    // Selection routed from app via setSelectedEntities to avoid mutating ECS flags for rendering
-                    params.selected = (m_selectedIds.find(entity->getId()) != m_selectedIds.end());
-                    fn(params);
-                    drawnByRegistry = true;
-                }
-            }
-        }
-        if (drawnByRegistry) {
-            // Draw rally flag marker if this is a barracks with a set rally
-            if (auto* unit = entity->getComponent<Engine::Core::UnitComponent>()) {
-                if (unit->unitType == "barracks") {
-                    if (auto* prod = entity->getComponent<Engine::Core::ProductionComponent>()) {
-                        if (prod->rallySet && m_resources) {
-                            QMatrix4x4 flagModel;
-                            flagModel.translate(prod->rallyX, 0.1f, prod->rallyZ);
-                            flagModel.scale(0.2f, 0.2f, 0.2f);
-                            drawMeshColored(m_resources->unit(), flagModel, QVector3D(1.0f, 0.9f, 0.2f), m_resources->white());
-                        }
-                    }
-                }
-            }
-            continue;
-        }
-
-        // Else choose mesh based on RenderableComponent hint
-        RenderCommand command;
-        command.modelMatrix = modelMatrix;
-        Mesh* meshToDraw = nullptr;
-        switch (renderable->mesh) {
-            case Engine::Core::RenderableComponent::MeshKind::Quad:    meshToDraw = m_resources? m_resources->quad() : nullptr; break;
-            case Engine::Core::RenderableComponent::MeshKind::Plane:   meshToDraw = m_resources? m_resources->ground() : nullptr; break;
-            case Engine::Core::RenderableComponent::MeshKind::Cube:    meshToDraw = m_resources? m_resources->unit() : nullptr; break;
-            case Engine::Core::RenderableComponent::MeshKind::Capsule: meshToDraw = nullptr; break; // handled by specific renderer when available
-            case Engine::Core::RenderableComponent::MeshKind::Ring:    meshToDraw = nullptr; break; // rings are drawn explicitly when needed
-            case Engine::Core::RenderableComponent::MeshKind::None:    default: break;
-        }
-        if (!meshToDraw && m_resources) meshToDraw = m_resources->unit();
-        if (!meshToDraw && m_resources) meshToDraw = m_resources->quad();
-        command.mesh = meshToDraw;
-        command.texture = (m_resources ? m_resources->white() : nullptr);
-        // Use per-entity color if set, else a default
-        command.color = QVector3D(renderable->color[0], renderable->color[1], renderable->color[2]);
-        submitRenderCommand(command);
-
-        // If this render path drew a barracks (no custom renderer used), also draw rally flag
-        if (auto* unit = entity->getComponent<Engine::Core::UnitComponent>()) {
-            if (unit->unitType == "barracks") {
-                if (auto* prod = entity->getComponent<Engine::Core::ProductionComponent>()) {
-                    if (prod->rallySet && m_resources) {
-                        QMatrix4x4 flagModel;
-                        flagModel.translate(prod->rallyX, 0.1f, prod->rallyZ);
-                        flagModel.scale(0.2f, 0.2f, 0.2f);
-                        drawMeshColored(m_resources->unit(), flagModel, QVector3D(1.0f, 0.9f, 0.2f), m_resources->white());
-                    }
-                }
-            }
-        }
-        // Selection ring is drawn by entity-specific renderer if desired
-    }
-}
-
-bool Renderer::loadShaders() {
-    // Load shaders from assets
-    const QString base = QStringLiteral("assets/shaders/");
-    const QString basicVert = base + QStringLiteral("basic.vert");
-    const QString basicFrag = base + QStringLiteral("basic.frag");
-    const QString gridFrag  = base + QStringLiteral("grid.frag");
-
-    m_basicShader = std::make_unique<Shader>();
-    if (!m_basicShader->loadFromFiles(basicVert, basicFrag)) {
-        qWarning() << "Failed to load basic shader from files" << basicVert << basicFrag;
-        return false;
-    }
-    m_gridShader = std::make_unique<Shader>();
-    // Reuse the same vertex shader for grid; load vertex from file and fragment from grid
-    if (!m_gridShader->loadFromFiles(basicVert, gridFrag)) {
-        qWarning() << "Failed to load grid shader from files" << basicVert << gridFrag;
-        m_gridShader.reset();
-    }
-    return true;
-}
-
-void Renderer::sortRenderQueue() {
-    // Simple sorting by texture to reduce state changes
-    std::sort(m_renderQueue.begin(), m_renderQueue.end(),
-        [](const RenderCommand& a, const RenderCommand& b) {
-            return a.texture < b.texture;
-        });
-}
-
-} // namespace Render::GL

+ 0 - 113
render/gl/renderer.h

@@ -1,113 +0,0 @@
-#pragma once
-
-#include "shader.h"
-#include "camera.h"
-#include "mesh.h"
-#include "texture.h"
-#include "resources.h"
-#include <QOpenGLFunctions_3_3_Core>
-#include <memory>
-#include <vector>
-#include <optional>
-#include <unordered_set>
-
-namespace Engine::Core {
-class World;
-class Entity;
-}
-
-namespace Render::GL {
-class EntityRendererRegistry;
-}
-
-namespace Game { namespace Systems { class ArrowSystem; } }
-
-namespace Render::GL {
-
-struct RenderCommand {
-    Mesh* mesh = nullptr;
-    Texture* texture = nullptr;
-    QMatrix4x4 modelMatrix;
-    QVector3D color{1.0f, 1.0f, 1.0f};
-};
-
-class Renderer : protected QOpenGLFunctions_3_3_Core {
-public:
-    Renderer();
-    ~Renderer();
-
-    bool initialize();
-    void shutdown();
-    
-    void beginFrame();
-    void endFrame();
-    void setViewport(int width, int height);
-    
-    void setCamera(Camera* camera);
-    void setClearColor(float r, float g, float b, float a = 1.0f);
-    // Optional: inject an external ResourceManager owned by the app
-    void setResources(const std::shared_ptr<ResourceManager>& resources) { m_resources = resources; }
-    void setHoveredBuildingId(unsigned int id) { m_hoveredBuildingId = id; }
-    // Selection information provided by the app before rendering
-    void setSelectedEntities(const std::vector<unsigned int>& ids) {
-        m_selectedIds.clear();
-        m_selectedIds.insert(ids.begin(), ids.end());
-    }
-
-    // Lightweight, app-facing helpers
-    void renderGridGround();
-    // High-level helpers are provided in render/entity (see arrow)
-
-    // Read-only access to default meshes/textures for app-side batching
-    Mesh* getMeshQuad()    const { return m_resources ? m_resources->quad()    : nullptr; }
-    Mesh* getMeshPlane()   const { return m_resources ? m_resources->ground()  : nullptr; }
-    Mesh* getMeshCube()    const { return m_resources ? m_resources->unit()    : nullptr; }
-    // Meshes now provided by entity-specific renderers or shared geom providers
-    Texture* getWhiteTexture() const { return m_resources ? m_resources->white() : nullptr; }
-    
-    struct GridParams {
-        float cellSize = 1.0f;
-        float thickness = 0.06f; // fraction of cell (0..0.5)
-        QVector3D gridColor{0.15f, 0.18f, 0.15f};
-        QVector3D lineColor{0.22f, 0.25f, 0.22f};
-        float extent = 50.0f; // half-size of plane scaling
-    };
-    void setGridParams(const GridParams& gp) { m_gridParams = gp; }
-    const GridParams& gridParams() const { return m_gridParams; }
-    
-    // Immediate mode rendering
-    void drawMesh(Mesh* mesh, const QMatrix4x4& modelMatrix, Texture* texture = nullptr);
-    void drawMeshColored(Mesh* mesh, const QMatrix4x4& modelMatrix, const QVector3D& color, Texture* texture = nullptr);
-    void drawLine(const QVector3D& start, const QVector3D& end, const QVector3D& color);
-    
-    // Batch rendering
-    void submitRenderCommand(const RenderCommand& command);
-    void flushBatch();
-    
-    // Legacy: still available but apps are encouraged to issue draw calls explicitly
-    void renderWorld(Engine::Core::World* world);
-    
-private:
-    Camera* m_camera = nullptr;
-    
-    std::unique_ptr<Shader> m_basicShader;
-    std::unique_ptr<Shader> m_lineShader;
-    std::unique_ptr<Shader> m_gridShader;
-    
-    std::vector<RenderCommand> m_renderQueue;
-    
-    // Default resources
-    std::shared_ptr<ResourceManager> m_resources;
-    std::unique_ptr<EntityRendererRegistry> m_entityRegistry;
-    unsigned int m_hoveredBuildingId = 0;
-    std::unordered_set<unsigned int> m_selectedIds; // for selection rings at render time
-
-    int m_viewportWidth = 0;
-    int m_viewportHeight = 0;
-    GridParams m_gridParams;
-    
-    bool loadShaders();
-    void sortRenderQueue();
-};
-
-} // namespace Render::GL

+ 5 - 0
render/gl/shader_cache.cpp

@@ -0,0 +1,5 @@
+#include "shader_cache.h"
+namespace Render::GL {}
+namespace Render::GL {
+// All logic is in the header for simplicity.
+}

+ 66 - 0
render/gl/shader_cache.h

@@ -0,0 +1,66 @@
+#pragma once
+
+#include <memory>
+#include <QString>
+#include <unordered_map>
+#include "shader.h"
+
+namespace Render::GL {
+
+class ShaderCache {
+public:
+    // Load and cache a shader under a friendly name
+    Shader* load(const QString& name, const QString& vertPath, const QString& fragPath) {
+        auto it = m_named.find(name);
+        if (it != m_named.end()) return it->second.get();
+        auto sh = std::make_unique<Shader>();
+        if (!sh->loadFromFiles(vertPath, fragPath)) return nullptr;
+        Shader* raw = sh.get();
+        m_named.emplace(name, std::move(sh));
+        return raw;
+    }
+
+    // Get a shader by name (nullptr if not loaded)
+    Shader* get(const QString& name) const {
+        auto it = m_named.find(name);
+        return (it != m_named.end()) ? it->second.get() : nullptr;
+    }
+
+    // Convenience for loading by paths without a name (kept for compatibility)
+    Shader* getOrLoad(const QString& vertPath, const QString& fragPath) {
+        auto key = vertPath + "|" + fragPath;
+        auto it = m_byPath.find(key);
+        if (it != m_byPath.end()) return it->second.get();
+        auto sh = std::make_unique<Shader>();
+        if (!sh->loadFromFiles(vertPath, fragPath)) return nullptr;
+        Shader* raw = sh.get();
+        m_byPath.emplace(std::move(key), std::move(sh));
+        return raw;
+    }
+
+    // Load the default built-in shaders and register them under friendly names
+    void initializeDefaults() {
+        static const QString kShaderBase = QStringLiteral("assets/shaders/");
+        const QString basicVert = kShaderBase + QStringLiteral("basic.vert");
+        const QString basicFrag = kShaderBase + QStringLiteral("basic.frag");
+        const QString gridFrag  = kShaderBase + QStringLiteral("grid.frag");
+        load(QStringLiteral("basic"), basicVert, basicFrag);
+        load(QStringLiteral("grid"),  basicVert, gridFrag);
+    }
+
+    void clear() {
+        m_named.clear();
+        m_byPath.clear();
+        m_cache.clear();
+    }
+
+private:
+    // Legacy path-keyed cache (optional)
+    std::unordered_map<QString, std::unique_ptr<Shader>> m_byPath;
+    // Named shaders (preferred)
+    std::unordered_map<QString, std::unique_ptr<Shader>> m_named;
+    // Backwards compatibility alias
+    std::unordered_map<QString, std::unique_ptr<Shader>> m_cache; // deprecated
+};
+
+} // namespace Render::GL

+ 5 - 0
render/gl/state_scopes.cpp

@@ -0,0 +1,5 @@
+#include "state_scopes.h"
+namespace Render::GL {}
+namespace Render::GL {
+// Scopes are inline-only patterns; nothing to compile here.
+}

+ 51 - 0
render/gl/state_scopes.h

@@ -0,0 +1,51 @@
+#pragma once
+#include <QOpenGLFunctions_3_3_Core>
+// Assumes a current GL context and that initializeOpenGLFunctions() was already
+// called once by the Backend. Avoid per-scope initialization overhead.
+namespace Render::GL {
+
+struct DepthMaskScope {
+    GLboolean prev;
+    explicit DepthMaskScope(bool enableWrite) {
+        glGetBooleanv(GL_DEPTH_WRITEMASK, &prev);
+        glDepthMask(enableWrite ? GL_TRUE : GL_FALSE);
+    }
+    ~DepthMaskScope() { glDepthMask(prev); }
+};
+
+struct PolygonOffsetScope {
+    GLboolean prevEnable;
+    float factor, units;
+    PolygonOffsetScope(float f, float u) : factor(f), units(u) {
+        prevEnable = glIsEnabled(GL_POLYGON_OFFSET_FILL);
+        glEnable(GL_POLYGON_OFFSET_FILL);
+        glPolygonOffset(factor, units);
+    }
+    ~PolygonOffsetScope() {
+        if (!prevEnable) glDisable(GL_POLYGON_OFFSET_FILL);
+        glPolygonOffset(0.0f, 0.0f);
+    }
+};
+
+struct BlendScope {
+    GLboolean prevEnable;
+    BlendScope(bool enable=true) {
+        prevEnable = glIsEnabled(GL_BLEND);
+        if (enable) glEnable(GL_BLEND); else glDisable(GL_BLEND);
+    }
+    ~BlendScope() {
+        if (prevEnable) glEnable(GL_BLEND); else glDisable(GL_BLEND);
+    }
+};
+
+struct DepthTestScope {
+    GLboolean prevEnable;
+    explicit DepthTestScope(bool enable) {
+        prevEnable = glIsEnabled(GL_DEPTH_TEST);
+        if (enable) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
+    }
+    ~DepthTestScope() {
+        if (prevEnable) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
+    }
+};
+}

+ 28 - 0
render/ground/ground_renderer.cpp

@@ -0,0 +1,28 @@
+#include "ground_renderer.h"
+#include "../scene_renderer.h"
+#include "../gl/resources.h"
+#include "../draw_queue.h"
+
+namespace Render { namespace GL {
+
+void GroundRenderer::recomputeModel() {
+    m_model.setToIdentity();
+    // Slightly lower ground to avoid z-fighting and occluding unit bases at y≈0
+    m_model.translate(0.0f, -0.02f, 0.0f);
+    if (m_width > 0 && m_height > 0) {
+        float scaleX = float(m_width) * m_tileSize * 0.5f;
+        float scaleZ = float(m_height) * m_tileSize * 0.5f;
+        m_model.scale(scaleX, 1.0f, scaleZ);
+    } else {
+        m_model.scale(m_extent, 1.0f, m_extent);
+    }
+}
+
+void GroundRenderer::submit(Renderer& renderer, ResourceManager& resources) {
+    // Submit a grid draw; backend will use the grid shader and draw the plane mesh
+    float cell = m_tileSize > 0.0f ? m_tileSize : 1.0f;
+    float extent = (m_width > 0 && m_height > 0) ? std::max(m_width, m_height) * m_tileSize * 0.5f : m_extent;
+    renderer.grid(m_model, m_color, cell, 0.06f, extent);
+}
+
+} }

+ 26 - 0
render/ground/ground_renderer.h

@@ -0,0 +1,26 @@
+#pragma once
+#include <QMatrix4x4>
+#include <QVector3D>
+
+namespace Render { namespace GL {
+class Renderer; class ResourceManager; class Mesh; class Texture;
+
+class GroundRenderer {
+public:
+    void configure(float tileSize, int width, int height) {
+        m_tileSize = tileSize; m_width = width; m_height = height; recomputeModel();
+    }
+    void configureExtent(float extent) { m_extent = extent; recomputeModel(); }
+    void setColor(const QVector3D& c) { m_color = c; }
+    void submit(Renderer& renderer, ResourceManager& resources);
+private:
+    void recomputeModel();
+    float m_tileSize = 1.0f;
+    int m_width = 50;
+    int m_height = 50;
+    float m_extent = 50.0f; // alternative config
+    QVector3D m_color{0.15f,0.18f,0.15f};
+    QMatrix4x4 m_model;
+};
+
+} }

+ 144 - 0
render/scene_renderer.cpp

@@ -0,0 +1,144 @@
+#include "scene_renderer.h"
+#include "gl/camera.h"
+#include "gl/resources.h"
+#include "gl/backend.h"
+#include "game/core/world.h"
+#include "game/core/component.h"
+#include <QDebug>
+#include <algorithm>
+#include <cmath>
+#include "entity/registry.h"
+
+namespace Render::GL {
+
+Renderer::Renderer() {}
+
+Renderer::~Renderer() {
+    shutdown();
+}
+
+bool Renderer::initialize() {
+    if (!m_backend) m_backend = std::make_shared<Backend>();
+    m_backend->initialize();
+    m_entityRegistry = std::make_unique<EntityRendererRegistry>();
+    registerBuiltInEntityRenderers(*m_entityRegistry);
+    return true;
+}
+
+void Renderer::shutdown() { m_backend.reset(); }
+
+void Renderer::beginFrame() {
+    if (m_backend) m_backend->beginFrame();
+    m_queue.clear();
+}
+
+void Renderer::endFrame() {
+    if (m_backend && m_camera) {
+        m_queue.sortForBatching();
+        m_backend->execute(m_queue, *m_camera);
+    }
+}
+
+void Renderer::setCamera(Camera* camera) {
+    m_camera = camera;
+}
+
+void Renderer::setClearColor(float r, float g, float b, float a) { if (m_backend) m_backend->setClearColor(r,g,b,a); }
+
+void Renderer::setViewport(int width, int height) {
+    m_viewportWidth = width;
+    m_viewportHeight = height;
+    if (m_backend) m_backend->setViewport(width, height);
+    if (m_camera && height > 0) {
+        float aspect = float(width) / float(height);
+        m_camera->setPerspective(m_camera->getFOV(), aspect, m_camera->getNear(), m_camera->getFar());
+    }
+}
+void Renderer::mesh(Mesh* mesh, const QMatrix4x4& model, const QVector3D& color,
+                    Texture* texture, float alpha) {
+    if (!mesh) return;
+    MeshCmd cmd; cmd.mesh = mesh; cmd.texture = texture; cmd.model = model; cmd.color = color; cmd.alpha = alpha;
+    m_queue.submit(cmd);
+}
+
+void Renderer::selectionRing(const QMatrix4x4& model, float alphaInner, float alphaOuter,
+                             const QVector3D& color) {
+    SelectionRingCmd cmd; cmd.model = model; cmd.alphaInner = alphaInner; cmd.alphaOuter = alphaOuter; cmd.color = color;
+    m_queue.submit(cmd);
+}
+
+void Renderer::grid(const QMatrix4x4& model, const QVector3D& color, float cellSize, float thickness, float extent) {
+    GridCmd cmd; cmd.model = model; cmd.color = color; cmd.cellSize = cellSize; cmd.thickness = thickness; cmd.extent = extent;
+    m_queue.submit(cmd);
+}
+
+void Renderer::selectionSmoke(const QMatrix4x4& model, const QVector3D& color, float baseAlpha) {
+    SelectionSmokeCmd cmd; cmd.model = model; cmd.color = color; cmd.baseAlpha = baseAlpha;
+    m_queue.submit(cmd);
+}
+
+// submitRenderCommand removed; use mesh() directly
+
+void Renderer::renderWorld(Engine::Core::World* world) {
+    if (!world) return;
+
+    // Hover ring is now entity-renderer-owned; we only pass hovered flag in context
+
+    // Get all entities with both transform and renderable components
+    auto renderableEntities = world->getEntitiesWith<Engine::Core::RenderableComponent>();
+
+    for (auto entity : renderableEntities) {
+        auto renderable = entity->getComponent<Engine::Core::RenderableComponent>();
+        auto transform = entity->getComponent<Engine::Core::TransformComponent>();
+
+        if (!renderable->visible || !transform) {
+            continue;
+        }
+
+    // Build model matrix from transform
+        QMatrix4x4 modelMatrix;
+        modelMatrix.translate(transform->position.x, transform->position.y, transform->position.z);
+        modelMatrix.rotate(transform->rotation.x, QVector3D(1, 0, 0));
+        modelMatrix.rotate(transform->rotation.y, QVector3D(0, 1, 0));
+        modelMatrix.rotate(transform->rotation.z, QVector3D(0, 0, 1));
+        modelMatrix.scale(transform->scale.x, transform->scale.y, transform->scale.z);
+
+        // If entity has a unitType, try registry-based renderer first
+        bool drawnByRegistry = false;
+        if (auto* unit = entity->getComponent<Engine::Core::UnitComponent>()) {
+            if (!unit->unitType.empty() && m_entityRegistry) {
+                auto fn = m_entityRegistry->get(unit->unitType);
+                if (fn) {
+                    DrawContext ctx{resources(), entity, modelMatrix};
+                    // Selection routed from app via setSelectedEntities to avoid mutating ECS flags for rendering
+                    ctx.selected = (m_selectedIds.find(entity->getId()) != m_selectedIds.end());
+                    ctx.hovered = (entity->getId() == m_hoveredBuildingId);
+                    fn(ctx, *this);
+                    drawnByRegistry = true;
+                }
+            }
+        }
+        if (drawnByRegistry) continue;
+
+        // Else choose mesh based on RenderableComponent hint and submit
+        Mesh* meshToDraw = nullptr;
+        ResourceManager* res = resources();
+        switch (renderable->mesh) {
+            case Engine::Core::RenderableComponent::MeshKind::Quad:    meshToDraw = res? res->quad() : nullptr; break;
+            case Engine::Core::RenderableComponent::MeshKind::Plane:   meshToDraw = res? res->ground() : nullptr; break;
+            case Engine::Core::RenderableComponent::MeshKind::Cube:    meshToDraw = res? res->unit() : nullptr; break;
+            case Engine::Core::RenderableComponent::MeshKind::Capsule: meshToDraw = nullptr; break; // handled by specific renderer when available
+            case Engine::Core::RenderableComponent::MeshKind::Ring:    meshToDraw = nullptr; break; // rings are drawn explicitly when needed
+            case Engine::Core::RenderableComponent::MeshKind::None:    default: break;
+        }
+        if (!meshToDraw && res) meshToDraw = res->unit();
+        if (!meshToDraw && res) meshToDraw = res->quad();
+        QVector3D color = QVector3D(renderable->color[0], renderable->color[1], renderable->color[2]);
+        mesh(meshToDraw, modelMatrix, color, res ? res->white() : nullptr, 1.0f);
+
+        // Rally flag drawing moved into barracks renderer
+        // Selection ring is drawn by entity-specific renderer if desired
+    }
+}
+
+} // namespace Render::GL

+ 106 - 0
render/scene_renderer.h

@@ -0,0 +1,106 @@
+#pragma once
+
+#include "gl/camera.h"
+#include "gl/mesh.h"
+#include "gl/texture.h"
+#include "gl/resources.h"
+#include "gl/backend.h" // Needed for resources() access in inline getters
+#include "draw_queue.h"
+#include "submitter.h"
+#include <memory>
+#include <vector>
+#include <optional>
+#include <unordered_set>
+
+namespace Engine::Core {
+class World;
+class Entity;
+}
+
+namespace Render::GL {
+class EntityRendererRegistry;
+}
+
+namespace Game { namespace Systems { class ArrowSystem; } }
+
+namespace Render::GL {
+
+class Backend;
+
+
+class Renderer : public ISubmitter {
+public:
+    Renderer();
+    ~Renderer();
+
+    bool initialize();
+    void shutdown();
+    
+    void beginFrame();
+    void endFrame();
+    void setViewport(int width, int height);
+    
+    void setCamera(Camera* camera);
+    void setClearColor(float r, float g, float b, float a = 1.0f);
+    // Resource access via backend-owned manager (read-only)
+    ResourceManager* resources() const { return m_backend ? m_backend->resources() : nullptr; }
+    void setHoveredBuildingId(unsigned int id) { m_hoveredBuildingId = id; }
+    // Selection information provided by the app before rendering
+    void setSelectedEntities(const std::vector<unsigned int>& ids) {
+        m_selectedIds.clear();
+        m_selectedIds.insert(ids.begin(), ids.end());
+    }
+
+    // High-level helpers are provided in render/entity (see arrow)
+
+    // Read-only access to default meshes/textures for app-side batching
+    Mesh* getMeshQuad()    const { return m_backend && m_backend->resources() ? m_backend->resources()->quad()   : nullptr; }
+    Mesh* getMeshPlane()   const { return m_backend && m_backend->resources() ? m_backend->resources()->ground() : nullptr; }
+    Mesh* getMeshCube()    const { return m_backend && m_backend->resources() ? m_backend->resources()->unit()   : nullptr; }
+    // Meshes now provided by entity-specific renderers or shared geom providers
+    Texture* getWhiteTexture() const { return m_backend && m_backend->resources() ? m_backend->resources()->white() : nullptr; }
+    // Optional: allow advanced renderers to obtain or load shaders lazily by name
+    Shader* getShader(const QString& name) const { return m_backend ? m_backend->shader(name) : nullptr; }
+    Shader* loadShader(const QString& name, const QString& vertPath, const QString& fragPath) {
+        return m_backend ? m_backend->getOrLoadShader(name, vertPath, fragPath) : nullptr;
+    }
+    
+    struct GridParams {
+        float cellSize = 1.0f;
+        float thickness = 0.06f; // fraction of cell (0..0.5)
+        QVector3D gridColor{0.15f, 0.18f, 0.15f};
+        float extent = 50.0f; // half-size of plane scaling
+    };
+    void setGridParams(const GridParams& gp) { m_gridParams = gp; }
+    const GridParams& gridParams() const { return m_gridParams; }
+    
+    // ISubmitter implementation (enqueue for backend)
+    void mesh(Mesh* mesh, const QMatrix4x4& model, const QVector3D& color,
+              Texture* texture = nullptr, float alpha = 1.0f) override;
+    void selectionRing(const QMatrix4x4& model, float alphaInner, float alphaOuter,
+                       const QVector3D& color) override;
+    // Enqueue a grid draw call
+    void grid(const QMatrix4x4& model, const QVector3D& color, float cellSize, float thickness, float extent) override;
+    // Enqueue a smoky selection overlay
+    void selectionSmoke(const QMatrix4x4& model, const QVector3D& color, float baseAlpha = 0.15f) override;
+    
+    // Legacy: still available but apps are encouraged to issue draw calls explicitly
+    void renderWorld(Engine::Core::World* world);
+    
+private:
+    Camera* m_camera = nullptr;
+    std::shared_ptr<Backend> m_backend;
+    DrawQueue m_queue;
+    
+    // Default resources
+    // Resources now owned by Backend
+    std::unique_ptr<EntityRendererRegistry> m_entityRegistry;
+    unsigned int m_hoveredBuildingId = 0;
+    std::unordered_set<unsigned int> m_selectedIds; // for selection rings at render time
+
+    int m_viewportWidth = 0;
+    int m_viewportHeight = 0;
+    GridParams m_gridParams;
+};
+
+} // namespace Render::GL

+ 55 - 0
render/submitter.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include <QMatrix4x4>
+#include <QVector3D>
+#include "draw_queue.h"
+
+namespace Render::GL { class Mesh; class Texture; }
+
+namespace Render::GL {
+
+class ISubmitter {
+public:
+    virtual ~ISubmitter() = default;
+    virtual void mesh(Mesh* mesh, const QMatrix4x4& model, const QVector3D& color,
+                      Texture* tex = nullptr, float alpha = 1.0f) = 0;
+    virtual void selectionRing(const QMatrix4x4& model, float alphaInner, float alphaOuter,
+                               const QVector3D& color) = 0;
+    virtual void grid(const QMatrix4x4& model, const QVector3D& color,
+                      float cellSize, float thickness, float extent) = 0;
+    virtual void selectionSmoke(const QMatrix4x4& model, const QVector3D& color,
+                                float baseAlpha = 0.15f) = 0;
+};
+
+class QueueSubmitter : public ISubmitter {
+public:
+    explicit QueueSubmitter(DrawQueue* queue) : m_queue(queue) {}
+    void mesh(Mesh* mesh, const QMatrix4x4& model, const QVector3D& color,
+              Texture* tex = nullptr, float alpha = 1.0f) override {
+        if (!m_queue || !mesh) return;
+        MeshCmd cmd; cmd.mesh = mesh; cmd.texture = tex; cmd.model = model; cmd.color = color; cmd.alpha = alpha;
+        m_queue->submit(cmd);
+    }
+    void selectionRing(const QMatrix4x4& model, float alphaInner, float alphaOuter,
+                       const QVector3D& color) override {
+        if (!m_queue) return;
+        SelectionRingCmd cmd; cmd.model = model; cmd.alphaInner = alphaInner; cmd.alphaOuter = alphaOuter; cmd.color = color;
+        m_queue->submit(cmd);
+    }
+    void grid(const QMatrix4x4& model, const QVector3D& color,
+              float cellSize, float thickness, float extent) override {
+        if (!m_queue) return;
+        GridCmd cmd; cmd.model = model; cmd.color = color; cmd.cellSize = cellSize; cmd.thickness = thickness; cmd.extent = extent;
+        m_queue->submit(cmd);
+    }
+    void selectionSmoke(const QMatrix4x4& model, const QVector3D& color,
+                        float baseAlpha = 0.15f) override {
+        if (!m_queue) return;
+        SelectionSmokeCmd cmd; cmd.model = model; cmd.color = color; cmd.baseAlpha = baseAlpha;
+        m_queue->submit(cmd);
+    }
+private:
+    DrawQueue* m_queue = nullptr;
+};
+
+} // namespace Render::GL

+ 24 - 15
ui/qml/Main.qml

@@ -57,11 +57,16 @@ ApplicationWindow {
         id: edgeScrollOverlay
         id: edgeScrollOverlay
         anchors.fill: parent
         anchors.fill: parent
         z: 2
         z: 2
-        property real threshold: 80    // px from edge where scrolling starts
-        property real maxSpeed: 1.2    // world units per tick at the very edge
+        // Horizontal edge-scroll sensitivity and speed
+        property real horzThreshold: 80     // px from left/right edge where scroll begins
+        property real horzMaxSpeed: 0.5     // world units per tick at the very edge (left/right)
+        // Vertical edge-scroll is intentionally less sensitive and slower
+        property real vertThreshold: 120    // px after dead-zone for top/bottom
+        property real verticalDeadZone: 32  // no scroll within this many px past HUD bars
+        property real vertMaxSpeed: 0.1     // world units per tick at the very edge (top/bottom)
         property real xPos: -1
         property real xPos: -1
         property real yPos: -1
         property real yPos: -1
-    // Shift vertical edge-scroll away from HUD panels by this many pixels
+        // Shift vertical edge-scroll away from HUD panels by this many pixels
     property int verticalShift: 6
     property int verticalShift: 6
         // Computed guard zones derived from HUD panel heights
         // Computed guard zones derived from HUD panel heights
         function inHudZone(x, y) {
         function inHudZone(x, y) {
@@ -130,7 +135,9 @@ ApplicationWindow {
                 }
                 }
                 // Keep hover updated even if positionChanged throttles
                 // Keep hover updated even if positionChanged throttles
                 if (game.setHoverAtScreen) game.setHoverAtScreen(x, y)
                 if (game.setHoverAtScreen) game.setHoverAtScreen(x, y)
-                const t = edgeScrollOverlay.threshold
+                const th = edgeScrollOverlay.horzThreshold
+                const tv = edgeScrollOverlay.vertThreshold
+                const vdz = edgeScrollOverlay.verticalDeadZone
                 const clamp = function(v, lo, hi) { return Math.max(lo, Math.min(hi, v)) }
                 const clamp = function(v, lo, hi) { return Math.max(lo, Math.min(hi, v)) }
                 // Distance from edges
                 // Distance from edges
                 const dl = x
                 const dl = x
@@ -140,21 +147,23 @@ ApplicationWindow {
                 const bottomBar = (typeof hud !== 'undefined' && hud && hud.bottomPanelHeight) ? hud.bottomPanelHeight : 0
                 const bottomBar = (typeof hud !== 'undefined' && hud && hud.bottomPanelHeight) ? hud.bottomPanelHeight : 0
                 const topEdge = topBar + edgeScrollOverlay.verticalShift
                 const topEdge = topBar + edgeScrollOverlay.verticalShift
                 const bottomEdge = h - bottomBar - edgeScrollOverlay.verticalShift
                 const bottomEdge = h - bottomBar - edgeScrollOverlay.verticalShift
-                const dt = Math.max(0, y - topEdge)
-                const db = Math.max(0, bottomEdge - y)
+                // Apply a vertical dead-zone beyond the HUD before scrolling starts
+                const dt = Math.max(0, (y - topEdge) - vdz)
+                const db = Math.max(0, (bottomEdge - y) - vdz)
                 // Normalized intensities (0..1)
                 // Normalized intensities (0..1)
-                const il = clamp(1.0 - dl / t, 0, 1)
-                const ir = clamp(1.0 - dr / t, 0, 1)
-                const iu = clamp(1.0 - dt / t, 0, 1)
-                const id = clamp(1.0 - db / t, 0, 1)
+                const il = clamp(1.0 - dl / th, 0, 1)
+                const ir = clamp(1.0 - dr / th, 0, 1)
+                const iu = clamp(1.0 - dt / tv, 0, 1)
+                const id = clamp(1.0 - db / tv, 0, 1)
                 if (il===0 && ir===0 && iu===0 && id===0) return
                 if (il===0 && ir===0 && iu===0 && id===0) return
                 // Apply gentle curve for smoother start
                 // Apply gentle curve for smoother start
-                const curve = function(a) { return a*a }
-                const maxS = edgeScrollOverlay.maxSpeed
-                const dx = (curve(ir) - curve(il)) * maxS
-                const dz = (curve(iu) - curve(id)) * maxS
+                const curveH = function(a) { return a*a }
+                // Vertical curve is gentler (cubic) to reduce early strength
+                const curveV = function(a) { return a*a*a }
+                const dx = (curveH(ir) - curveH(il)) * edgeScrollOverlay.horzMaxSpeed
+                const dz = (curveV(iu) - curveV(id)) * edgeScrollOverlay.vertMaxSpeed
                 if (dx !== 0 || dz !== 0) game.cameraMove(dx, dz)
                 if (dx !== 0 || dz !== 0) game.cameraMove(dx, dz)
             }
             }
         }
         }
     }
     }
-}
+}