0
\$\begingroup\$

I am currently designing the scripting side of our engine. The idea is an entity-component system. That means most of the coding is done via jobs.

The language we are using is compatible to C. The actual implementation does not matter for this question. Basically, the application - written in C++ - calls the entry point for our scripting environment. User code is added to this environment, generating one library for client code and our code. Via some engine magic we then make the assembly aware of the jobs that were written by users. So far so good.

The design decision we are currently struggling with is how the scripting interacts with the renderer.

For that, a short brief of our architecture:

  • Every module (Rendering, Audio, Physics) is laid out into dynamic libraries, so our scripting language can interact with them.
  • Rendering is API-agnostic. Currently we have OpenGL and Dx12 in the making, OpenGL will get removed by Vulkan on the long run.
  • The scripting language handles everything. Only the high-performance critical systems are C++ dynamic libraries.

That having said, when interacting with the renderer we have two concepts. We want to decide which one to implement, but are unsure. The techniques here are simplified, in the actual engine there are of course some managers etc. going on.

    1. Every mesh instance issues a drawcall once per frame.
    • The renderer has a draw method
    • Requires fewer instructions
    • Every mesh calls draw for themselves, once per frame
    • Does mean that some meshes will be drawn before other entities even had their logic running
    1. Every mesh instance issues a draw request once per frame.
    • The renderer has a render method
    • The assembly calls render after all jobs is finished
    • The GPU can run every drawcall at once
    • The GPU can only start rendering once the whole frame is finished on the assembly side.

Each of the systems has their pros and cons. Personally, system 2 makes the most sense to me as the renderer is fully responsible for rendering and only gets instructed. Furthermore, APIs like DX12 and Vulkan are already command-based, so this follows the API rules in some way.

Though I have seen many people do the drawcalls on a per mesh basis. There might be some advantages to that, but I feel like architecture 2 is more robust and scalable.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

The second approach is better in general (with the exception of code that generates a new mesh each frame). The reason is that queueing request allows you to execute them in batch. And that opens plenty of optimizations. For example, use a single render call for many identical meshes. And if you are switching shaders, you can make of the render calls for each shader together, minimizing the number of times you switch shaders. You might even compile multiple meshes in a single buffer, so they can be a single call despite not being identical.

Just one note: make it transparent for the developer writing the scripts, they don't need to know about these or other similar optimizations (which would differ per rendering backend, by the way). And if it is going to be transparent, you can start by implementing the first approach (which is easier to implement), locking the interface, and switch to the second approach, without breaking scripting. In general, changes to the renderer should not break scripting. Design with that in mind.

Perhaps you are deciding between a push or a pull interface. For example, if the script will call a method to request something to be rendered, you have a push interface. On the other hand, if you have the script define a function that the rendering system calls and return whatever should be rendered, you have a pull interface.

Well, you say you are going to use an entity-component-system architecture. Thus, I believe it follows that scripts could attach a mesh component, and then you have the renderer system runs for each entity that has a mesh component attached and draw it queue it for rendering.

\$\endgroup\$
1
  • \$\begingroup\$ Exactly, the last section sums it up. The idea is to have a job that checks each entity for a MeshComponent and queues it for rendering. After all scripts have finished their job for the current frame, the render method is called. So basically push. \$\endgroup\$ Commented Jan 16, 2022 at 12:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .