Skip to content

Commit

Permalink
rework Renderer/RenderPass implementations to reduce heap allocations…
Browse files Browse the repository at this point in the history
… and memory usage, add Text class that allows shaping before rendering any glyphs, fix potential text rendering artifacts on first frame, add built-in quad/cube models, add options for overriding texture and material parameters when drawing models, change names of default textures and reduce default specular map intensity
  • Loading branch information
DonutVikingChap committed Sep 23, 2023
1 parent 76995b3 commit 254be9b
Show file tree
Hide file tree
Showing 33 changed files with 2,446 additions and 780 deletions.
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ if(DONUT_ENABLE_LIBRARY)
"include/donut/graphics/ShaderProgram.hpp"
"include/donut/graphics/ShaderStage.hpp"
"include/donut/graphics/SpriteAtlas.hpp"
"include/donut/graphics/Text.hpp"
"include/donut/graphics/Texture.hpp"
"include/donut/graphics/TexturedQuad.hpp"
"include/donut/graphics/VertexArray.hpp"
Expand Down Expand Up @@ -76,9 +77,11 @@ if(DONUT_ENABLE_LIBRARY)
"include/donut/Filesystem.hpp"
"include/donut/json.hpp"
"include/donut/LinearAllocator.hpp"
"include/donut/LinearBuffer.hpp"
"include/donut/LooseQuadtree.hpp"
"include/donut/math.hpp"
"include/donut/obj.hpp"
"include/donut/Overloaded.hpp"
"include/donut/random.hpp"
"include/donut/reflection.hpp"
"include/donut/shapes.hpp"
Expand Down Expand Up @@ -111,6 +114,7 @@ if(DONUT_ENABLE_LIBRARY)
"src/graphics/ShaderParameter.cpp"
"src/graphics/ShaderProgram.cpp"
"src/graphics/ShaderStage.cpp"
"src/graphics/Text.cpp"
"src/graphics/Texture.cpp"
"src/graphics/VertexArray.cpp"
"src/graphics/Window.cpp"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Libdonut is an application framework for cross-platform game development in C++2
- 3D Model rendering that supports custom shaders through [Shader3D](include/donut/graphics/Shader3D.hpp) or basic built-in Blinn-Phong lighting for prototyping.
- 2D Textured quad rendering with built-in shaders or custom shaders through [Shader2D](include/donut/graphics/Shader2D.hpp).
- Sprite rendering with automatic [SpriteAtlas](include/donut/graphics/SpriteAtlas.hpp) packing.
- Text rendering and [Font](include/donut/graphics/Font.hpp) loading using [libschrift](https://github.com/tomolt/libschrift).
- [Text](include/donut/graphics/Text.hpp) rendering and [Font](include/donut/graphics/Font.hpp) loading using [libschrift](https://github.com/tomolt/libschrift).
- Supports arbitrary [Framebuffer](include/donut/graphics/Framebuffer.hpp) targets, [Camera](include/donut/graphics/Camera.hpp) positions and [Viewport](include/donut/graphics/Viewport.hpp) areas.
- Viewports can be restricted to integer scaling for pixel-perfect fixed-resolution 2D rendering regardless of window size.
- [Model](include/donut/graphics/Model.hpp) loading from OBJ files.
Expand Down
3 changes: 2 additions & 1 deletion docs/mainpage.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The main API of libdonut is organized into the following modules, listed along w
- [InputManager](@ref donut::events::InputManager) - Mapping between physical inputs and abstract output numbers
- [donut::graphics](@ref donut::graphics) - Graphics rendering module
- [Camera](@ref donut::graphics::Camera) - Perspective to render from
- [Font](@ref donut::graphics::Font) - Text shaping facility
- [Font](@ref donut::graphics::Font) - Font loading for text rendering
- [Framebuffer](@ref donut::graphics::Framebuffer) - Render target on the GPU
- [Image](@ref donut::graphics::Image) - Image loading/saving
- [Renderer](@ref donut::graphics::Renderer) - Rendering onto a framebuffer
Expand All @@ -36,6 +36,7 @@ The main API of libdonut is organized into the following modules, listed along w
- [Shader2D](@ref donut::graphics::Shader2D) - Shader program for instanced 2D textured quads
- [Shader3D](@ref donut::graphics::Shader3D) - Shader program for instanced 3D models
- [SpriteAtlas](@ref donut::graphics::SpriteAtlas) - Packing of sprites into an expandable spritesheet
- [Text](@ref donut::graphics::Text) - Text shaping facility
- [Texture](@ref donut::graphics::Texture) - Texture data stored on the GPU
- [Viewport](@ref donut::graphics::Viewport) - Rectangular region of a framebuffer
- [Window](@ref donut::graphics::Window) - Graphical window that can be rendered to
Expand Down
3 changes: 2 additions & 1 deletion examples/data/models/carrot_cake.mtl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
newmtl carrot_cake
illum 2
map_Kd carrot_cake_diffuse.jpg
map_Ns carrot_cake_specular.hdr
map_Ks carrot_cake_specular.hdr
map_bump carrot_cake_normal.hdr
Ns 92
Ks 4 4 4
133 changes: 77 additions & 56 deletions examples/example_game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
#include <charconv> // std::from_chars_result, std::from_chars
#include <chrono> // std::chrono_literals
#include <concepts> // std::integral
#include <cstddef> // std::size_t
#include <cstddef> // std::size_t, std::byte, std::max_align_t
#include <cstdio> // stderr, std::sscanf, std::fprintf
#include <cstdlib> // EXIT_SUCCESS, EXIT_FAILURE
#include <exception> // std::exception
Expand Down Expand Up @@ -135,26 +135,22 @@ class ExampleGame final : public app::Application {

renderer.clearFramebufferColorAndDepth(framebuffer, Color::PURPLE * 0.25f);

{
gfx::RenderPass renderPass{};
drawBackground(renderPass, frameInfo);
renderer.render(framebuffer, renderPass, worldViewport, worldCamera);
}
alignas(std::max_align_t) std::array<std::byte, 1024> renderPassStorage;

{
gfx::RenderPass renderPass{};
gfx::RenderPass renderPass{renderPassStorage};
drawWorld3D(renderPass, frameInfo);
renderer.render(framebuffer, renderPass, worldViewport, worldCamera);
}

{
gfx::RenderPass renderPass{};
gfx::RenderPass renderPass{renderPassStorage};
drawWorld2D(renderPass, frameInfo);
renderer.render(framebuffer, renderPass, screenViewport, screenCamera, worldScissor);
}

{
gfx::RenderPass renderPass{};
gfx::RenderPass renderPass{renderPassStorage};
drawUserInterface(renderPass, frameInfo);
drawFrameRateCounter(renderPass);
renderer.render(framebuffer, renderPass, screenViewport, screenCamera);
Expand Down Expand Up @@ -198,7 +194,7 @@ class ExampleGame final : public app::Application {

ExampleShader2D()
: gfx::Shader2D({
.vertexShaderSourceCode = gfx::Shader2D::vertexShaderSourceCodeInstancedTexturedQuad,
.vertexShaderSourceCode = gfx::Shader2D::VERTEX_SHADER_SOURCE_CODE_INSTANCED_TEXTURED_QUAD,
.fragmentShaderSourceCode = FRAGMENT_SHADER_SOURCE_CODE,
}) {}

Expand Down Expand Up @@ -262,6 +258,8 @@ class ExampleGame final : public app::Application {
in vec3 fragmentBitangent;
in vec2 fragmentTextureCoordinates;
in vec4 fragmentTintColor;
in vec3 fragmentSpecularFactor;
in vec3 fragmentEmissiveFactor;
out vec4 outputColor;
Expand Down Expand Up @@ -313,9 +311,9 @@ class ExampleGame final : public app::Application {
void main() {
vec4 sampledDiffuse = texture(diffuseMap, fragmentTextureCoordinates);
vec4 diffuse = fragmentTintColor * vec4(diffuseColor, 1.0 - dissolveFactor) * vec4(pow(sampledDiffuse.rgb, vec3(GAMMA)), sampledDiffuse.a);
vec3 specular = specularColor * texture(specularMap, fragmentTextureCoordinates).rgb;
vec3 emissive = emissiveColor * texture(emissiveMap, fragmentTextureCoordinates).rgb;
vec3 specular = fragmentSpecularFactor * specularColor * texture(specularMap, fragmentTextureCoordinates).rgb;
vec3 emissive = fragmentEmissiveFactor * emissiveColor * texture(emissiveMap, fragmentTextureCoordinates).rgb;
mat3 TBN = mat3(normalize(fragmentTangent), normalize(fragmentBitangent), normalize(fragmentNormal));
vec3 surfaceNormal = normalScale * (texture(normalMap, fragmentTextureCoordinates).xyz * 2.0 - vec3(1.0));
vec3 normal = normalize(TBN * surfaceNormal);
Expand All @@ -336,7 +334,7 @@ class ExampleGame final : public app::Application {
ExampleShader3D()
: gfx::Shader3D({
.definitions = fmt::format("#define POINT_LIGHT_COUNT {}", POINT_LIGHT_COUNT).c_str(),
.vertexShaderSourceCode = gfx::Shader3D::vertexShaderSourceCodeInstancedModel,
.vertexShaderSourceCode = gfx::Shader3D::VERTEX_SHADER_SOURCE_CODE_INSTANCED_MODEL,
.fragmentShaderSourceCode = FRAGMENT_SHADER_SOURCE_CODE,
}) {}

Expand Down Expand Up @@ -524,37 +522,53 @@ class ExampleGame final : public app::Application {
exampleShader3D.setTintTexture(&testTexture);
}

void drawBackground(gfx::RenderPass& renderPass, const app::FrameInfo& frameInfo) {
void drawWorld3D(gfx::RenderPass& renderPass, const app::FrameInfo& frameInfo) {
constexpr vec3 BACKGROUND_OFFSET{0.0f, 3.5f, -10.0f};
constexpr vec2 BACKGROUND_SCALE{18.0f, 18.0f};
constexpr vec2 BACKGROUND_SCALE{9.0f, 9.0f};
constexpr float BACKGROUND_ANGLE = -30.0f;
constexpr float BACKGROUND_SPEED = 2.0f;

renderPass.draw(gfx::QuadInstance{
.texture = &testTexture,
renderPass.draw(gfx::ModelInstance{
.shader = &exampleShader3D,
.model = gfx::Model::QUAD,
.diffuseMapOverride = &testTexture,
.transformation = translate(BACKGROUND_OFFSET) * //
orientate4(vec3{radians(BACKGROUND_ANGLE), 0.0f, 0.0f}) * //
scale(vec3{BACKGROUND_SCALE, 1.0f}) * //
translate(vec3{-0.5f, -0.5f, 0.0f}),
scale(vec3{BACKGROUND_SCALE, 1.0f}),
.textureOffset{0.0f, frameInfo.elapsedTime * BACKGROUND_SPEED},
.textureScale = 1000.0f * BACKGROUND_SCALE / testTexture.getSize2D(),
});
}

void drawWorld3D(gfx::RenderPass& renderPass, const app::FrameInfo& frameInfo) {
renderPass.draw(gfx::ModelInstance{
.shader = &exampleShader3D,
.model = &carrotCakeModel,
.transformation = translate(vec3{0.6f, 0.7f, -3.0f} + carrotCakeDisplayPosition) * //
scale(vec3{5.0f * carrotCakeScale.x, 5.0f * carrotCakeScale.y, 5.0f}) * //
.transformation = translate(vec3{-0.6f, 0.2f, -3.0f}) * //
scale(vec3{5.0f, 5.0f, 5.0f}) * //
orientate4(vec3{0.0f, frameInfo.elapsedTime * 1.5f, frameInfo.elapsedTime * 2.0f}) * //
translate(vec3{0.0f, -0.05f, 0.0f}),
});

renderPass.draw(gfx::ModelInstance{
.shader = &exampleShader3D,
.model = &carrotCakeModel,
.transformation = translate(vec3{-0.6f, 0.2f, -3.0f}) * //
.transformation = translate(vec3{-0.5f, -2.0f, -5.0f}) * //
scale(vec3{5.0f, 5.0f, 5.0f}) * //
orientate4(vec3{frameInfo.elapsedTime * 2.0f, frameInfo.elapsedTime * 1.5f, 0.0f}) * //
translate(vec3{0.0f, -0.05f, 0.0f}),
});

renderPass.draw(gfx::ModelInstance{
.model = gfx::Model::CUBE,
.transformation = translate(vec3{1.0f, -1.0f, -5.0f}) * //
scale(vec3{0.5f, 0.5, 0.5f}) * //
orientate4(vec3{frameInfo.elapsedTime * 2.0f, frameInfo.elapsedTime * 1.5f, 0.0f}),
.tintColor = Color::RED,
});

renderPass.draw(gfx::ModelInstance{
.model = &carrotCakeModel,
.transformation = translate(vec3{0.6f, 0.7f, -3.0f} + carrotCakeDisplayPosition) * //
scale(vec3{5.0f * carrotCakeScale.x, 5.0f * carrotCakeScale.y, 5.0f}) * //
orientate4(vec3{0.0f, frameInfo.elapsedTime * 1.5f, frameInfo.elapsedTime * 2.0f}) * //
translate(vec3{0.0f, -0.05f, 0.0f}),
});
Expand Down Expand Up @@ -615,60 +629,51 @@ class ExampleGame final : public app::Application {
});

renderPass.draw(gfx::TextInstance{
.font = &mainFont,
.text = mainFont.shapeText(renderer, 8,
u8"The quick brown fox\n"
"jumps over the lazy dog\n"
"\n"
"FLYGANDE BÄCKASINER SÖKA\n"
"HWILA PÅ MJUKA TUVOR QXZ\n"
"0123456789\n"
"\n"
"+!\"#%&/()=?`@${[]}\\\n"
"~\'<>|,.-;:_"),
.text = &longTestText,
.position{410.0f, 416.0f},
.color = Color::LIME,
});

renderPass.draw(gfx::TextInstance{
renderPass.draw(gfx::TextStringInstance{
.font = &mainFont,
.text = mainFont.shapeText(renderer, 8,
fmt::format("Position:\n({:.2f}, {:.2f}, {:.2f})\n\nScale:\n({:.2f}, {:.2f})", carrotCakeDisplayPosition.x, carrotCakeDisplayPosition.y,
carrotCakeDisplayPosition.z, carrotCakeScale.x, carrotCakeScale.y)),
.characterSize = 8,
.position{410.0f, 310.0f},
.string = fmt::format("Position:\n({:.2f}, {:.2f}, {:.2f})\n\nScale:\n({:.2f}, {:.2f})", carrotCakeDisplayPosition.x, carrotCakeDisplayPosition.y,
carrotCakeDisplayPosition.z, carrotCakeScale.x, carrotCakeScale.y),
});

if (inputManager.isPressed(Action::MOVE_UP) || inputManager.justPressed(Action::MOVE_UP)) {
renderPass.draw(gfx::TextInstance{.font = &mainFont, .text = mainFont.shapeText(renderer, 8, "^"), .position{590.0f, 320.0f}});
renderPass.draw(gfx::TextInstance{.text = &upArrowText, .position{590.0f, 320.0f}});
}
if (inputManager.isPressed(Action::MOVE_DOWN) || inputManager.justPressed(Action::MOVE_DOWN)) {
renderPass.draw(gfx::TextInstance{.font = &mainFont, .text = mainFont.shapeText(renderer, 8, "v"), .position{590.0f, 300.0f}});
renderPass.draw(gfx::TextInstance{.text = &downArrowText, .position{590.0f, 300.0f}});
}
if (inputManager.isPressed(Action::MOVE_LEFT) || inputManager.justPressed(Action::MOVE_LEFT)) {
renderPass.draw(gfx::TextInstance{.font = &mainFont, .text = mainFont.shapeText(renderer, 8, "<"), .position{580.0f, 310.0f}});
renderPass.draw(gfx::TextInstance{.text = &leftArrowText, .position{580.0f, 310.0f}});
}
if (inputManager.isPressed(Action::MOVE_RIGHT) || inputManager.justPressed(Action::MOVE_RIGHT)) {
renderPass.draw(gfx::TextInstance{.font = &mainFont, .text = mainFont.shapeText(renderer, 8, ">"), .position{600.0f, 310.0f}});
renderPass.draw(gfx::TextInstance{.text = &rightArrowText, .position{600.0f, 310.0f}});
}

if (inputManager.isPressed(Action::AIM_UP) || inputManager.justPressed(Action::AIM_UP)) {
renderPass.draw(gfx::TextInstance{.font = &mainFont, .text = mainFont.shapeText(renderer, 8, "^"), .position{590.0f, 280.0f}});
renderPass.draw(gfx::TextInstance{.text = &upArrowText, .position{590.0f, 280.0f}});
}
if (inputManager.isPressed(Action::AIM_DOWN) || inputManager.justPressed(Action::AIM_DOWN)) {
renderPass.draw(gfx::TextInstance{.font = &mainFont, .text = mainFont.shapeText(renderer, 8, "v"), .position{590.0f, 260.0f}});
renderPass.draw(gfx::TextInstance{.text = &downArrowText, .position{590.0f, 260.0f}});
}
if (inputManager.isPressed(Action::AIM_LEFT) || inputManager.justPressed(Action::AIM_LEFT)) {
renderPass.draw(gfx::TextInstance{.font = &mainFont, .text = mainFont.shapeText(renderer, 8, "<"), .position{580.0f, 270.0f}});
renderPass.draw(gfx::TextInstance{.text = &leftArrowText, .position{580.0f, 270.0f}});
}
if (inputManager.isPressed(Action::AIM_RIGHT) || inputManager.justPressed(Action::AIM_RIGHT)) {
renderPass.draw(gfx::TextInstance{.font = &mainFont, .text = mainFont.shapeText(renderer, 8, ">"), .position{600.0f, 270.0f}});
renderPass.draw(gfx::TextInstance{.text = &rightArrowText, .position{600.0f, 270.0f}});
}

renderPass.draw(gfx::TextInstance{
renderPass.draw(gfx::TextStringInstance{
.font = &mainFont,
.text = mainFont.shapeText(renderer, 8,
fmt::format("Timer A: {:.2f}\nCounter A: {}\n\nTimer B: {:.2f}\nCounter B: {}", static_cast<float>(timerA), counterA, static_cast<float>(timerB), counterB)),
.characterSize = 8,
.position{410.0f, 240.0f},
.string =
fmt::format("Timer A: {:.2f}\nCounter A: {}\n\nTimer B: {:.2f}\nCounter B: {}", static_cast<float>(timerA), counterA, static_cast<float>(timerB), counterB),
});

if (inputManager.isPressed(events::Input::KEY_SPACE)) {
Expand Down Expand Up @@ -755,11 +760,12 @@ class ExampleGame final : public app::Application {
return intersects(movingCircleAabb, looseBounds);
});

renderPass.draw(gfx::TextInstance{
renderPass.draw(gfx::TextStringInstance{
.font = &mainFont,
.text = mainFont.shapeText(renderer, 8, fmt::format("AABB tests: {}\nCircle tests: {}", aabbTestCount, circleTestCount)),
.characterSize = 8,
.position{410.0f, 450.0f},
.color = Color::BURLY_WOOD,
.string = fmt::format("AABB tests: {}\nCircle tests: {}", aabbTestCount, circleTestCount),
});
}

Expand All @@ -770,11 +776,11 @@ class ExampleGame final : public app::Application {

void drawFrameRateCounter(gfx::RenderPass& renderPass) {
const unsigned fps = getLastSecondFrameCount();
const gfx::Font::ShapedText fpsText = mainFont.shapeText(renderer, 16, fmt::format("FPS: {}", fps));
frameRateCounterText.reshape(mainFont, 8, fmt::format("FPS: {}", fps), {0.0f, 0.0f}, {2.0f, 2.0f});
const vec2 fpsPosition{15.0f + 2.0f, 480.0f - 15.0f - 20.0f};
const Color fpsColor = (fps < 60) ? Color::RED : (fps < 120) ? Color::YELLOW : (fps < 240) ? Color::GRAY : Color::LIME;
renderPass.draw(gfx::TextInstance{.font = &mainFont, .text = fpsText, .position = fpsPosition + vec2{1.0f, -1.0f}, .color = Color::BLACK});
renderPass.draw(gfx::TextInstance{.font = &mainFont, .text = fpsText, .position = fpsPosition, .color = fpsColor});
renderPass.draw(gfx::TextInstance{.text = &frameRateCounterText, .position = fpsPosition + vec2{1.0f, -1.0f}, .color = Color::BLACK});
renderPass.draw(gfx::TextInstance{.text = &frameRateCounterText, .position = fpsPosition, .color = fpsColor});
}

events::EventPump eventPump{};
Expand All @@ -793,6 +799,21 @@ class ExampleGame final : public app::Application {
gfx::SpriteAtlas::SpriteId testSprite;
gfx::SpriteAtlas::SpriteId testSubSprite;
gfx::Font mainFont;
gfx::Text longTestText{mainFont, 8,
"The quick brown fox\n"
"jumps over the lazy dog\n"
"\n"
"FLYGANDE BÄCKASINER SÖKA\n"
"HWILA PÅ MJUKA TUVOR QXZ\n"
"0123456789\n"
"\n"
"+!\"#%&/()=?`@${[]}\\\n"
"~\'<>|,.-;:_"};
gfx::Text upArrowText{mainFont, 8, "^"};
gfx::Text downArrowText{mainFont, 8, "v"};
gfx::Text leftArrowText{mainFont, 8, "<"};
gfx::Text rightArrowText{mainFont, 8, ">"};
gfx::Text frameRateCounterText{};
ExampleShader2D exampleShader2D{};
ExampleShader3D exampleShader3D{};
events::InputManager inputManager{};
Expand Down
13 changes: 9 additions & 4 deletions include/donut/LinearAllocator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class LinearMemoryResource {
explicit LinearMemoryResource(std::span<std::byte> initialMemory) noexcept
: remainingMemoryBegin(initialMemory.data())
, remainingMemorySize(initialMemory.size())
, nextChunkSize(std::max(std::size_t{1024}, initialMemory.size() * GROWTH_FACTOR)) {}
, nextChunkSize(std::max(std::size_t{1024}, initialMemory.size() + initialMemory.size() / 2)) {}

void* allocate(std::size_t size, std::size_t alignment) {
if (size == 0) {
Expand All @@ -32,17 +32,24 @@ class LinearMemoryResource {
[[unlikely]];
const std::size_t newChunkSize = std::max(size, nextChunkSize);
const std::size_t newChunkAlignment = std::max(alignment, alignof(std::max_align_t));
if (extraMemory.capacity() < 4) {
extraMemory.reserve(4);
}
AlignedHeapMemoryChunk& newChunk = extraMemory.emplace_back(newChunkSize, newChunkAlignment);
remainingMemoryBegin = newChunk.memory;
remainingMemorySize = newChunkSize;
nextChunkSize *= GROWTH_FACTOR;
nextChunkSize += nextChunkSize / 2;
result = remainingMemoryBegin;
}
remainingMemoryBegin = static_cast<std::byte*>(remainingMemoryBegin) + size;
remainingMemorySize -= size;
return result;
}

[[nodiscard]] std::size_t getRemainingCapacity() const noexcept {
return remainingMemorySize;
}

private:
struct AlignedHeapMemoryChunk {
void* memory;
Expand Down Expand Up @@ -71,8 +78,6 @@ class LinearMemoryResource {
}
};

static constexpr std::size_t GROWTH_FACTOR = 2;

void* remainingMemoryBegin;
std::size_t remainingMemorySize;
std::size_t nextChunkSize;
Expand Down
Loading

0 comments on commit 254be9b

Please sign in to comment.