I prefer generic design where graphic engine only consist of a RenderQue, RenderParamTracker and TokenProcessor (meshes, materials and vertex definitions have those). INI file has definition of render modes, render targets, render params, token rules, etc...
It's tiny and efficient and you don't have to touch engine code ever (if implemented properly)
Essentially something like this.
My current design seperates the engine's data from it's own.When The engine calls the renderer, the rendering logic does not need to know about the particulars of the actual engine's data. Instead, it's api recieves copies and transforms the data to a state that is needed.
When it comes time to render, the culling system will work independently of the current game state, it'll display a latent frame instead, and cull data based on what it has. This also means that the Renderer uses it's own octree for it's own processes. Primarily culling, But also as a way of determining some broader spectrum of LOD.
The engine's logic has it's own octree for logical processes. RayCasting, Scripts that effect certain regions of land, Navmesh Collisions, etc.
For occlusion culling I'm using view frustum culling + Hi-Z algorithm (actually it's Lo-Z because of inverted z-buffer).
Main engine script does this:
SetRenderMode("PrepForHiZ"); // This will invoke setting of render targets, rendering quads, and after rendering is done, setting shader textures of those render targets, etc...
RenderOcclussionSpheres(); // this is main engine command - it keeps track of current objects (each has his own id and pos/radius)
void * flags = LockGraphicBuffer("occlusion_test_render_target", (X + 10)%10); // I'm having 10 frames delay (and 10 occlusion buffers)
SetOcclussionFlagsForObjects(flags);
Graphic engine doesn't even know that it did occlusion calculation. It doesn't understand what data sent to it means - only how to send it to graphic card.
That way it can be forward renderer, deffered rendered, forward+ rendered, ray tracer, <some new renderer that hasn't been invented yet>. It doesn't care what the data is - only how to render it efficiently.
As for token processor:
Rendering_technique_name = <Render Mode Name> + <Remaining Mesh Tokens> + <Remaining Material Tokens> + <Remaining Vertex Tokens>
SAMPLE: When rendering depth, there is no need to have tokens that have something to do with color. Then if all texture tokens are removed from material, Vertex token that represents texture coordinates will be removed (if there is no alpha mask texture). At the end you end up with only few tokens. Vertex token that represent NORMAL will probably also be removed (since it isn't even registered for RenderDepth render mode).
Once you know rendering technique name, you know which render parameters need to be sent to graphic card - and RenderParamTracker does that efficiently (without repetition).
The point is: there is no harcoding of anything graphic wise (shaders + graphic data come with the game files - not engine).
You write graphic engine once and then you don't touch it for years.
If some new way of rendering/post process effect gets published you don't change the engine - you just stuff the shader into first game package and add a few lines in the INI file. Maybe add few SetRenderModes("blablabla") into rendering script for some new post-processes.