Skip to content

80. Efficient Rendering

Learn how to write efficient rendering programs in Siv3D.

80.1 Rendering Load Indicators

  • For simple 2D rendering, inefficient programs rarely affect performance, but when performing large-scale rendering in complex games, you need to be mindful of rendering load
  • To reduce CPU and GPU rendering load in drawing processes, aim to minimize the following three indicators

80.1.1 Draw Call Count

  • The number of drawing commands issued by the Siv3D engine to the GPU
  • Note that this is different from the number of .draw() calls
  • When the program calls consecutive .draw() methods, if they use common render settings or textures, they are grouped into a single draw call
  • The draw call count from the previous frame can be obtained with Profiler::GetStat().drawCalls
  • Even for complex games, aim to keep draw calls below a few hundred

80.1.2 Triangle Count

  • The number of triangles drawn by the GPU
  • For example, Rect draws 2 triangles, Font text rendering draws 2 triangles per character, and Circle draws 10-200 triangles depending on size
  • The triangle count from the previous frame can be obtained with Profiler::GetStat().triangleCount
  • If the draw count exceeds tens of thousands, consider whether there are more efficient drawing methods

80.1.3 Cumulative Pixels Drawn

  • The cumulative number of pixels painted on screen by each .draw() call
  • Off-screen areas are not included. Background fills by Scene::SetBackground() are not counted
  • For example, the following drawing paints 400 x 600 = 240,000 pixels (excluding off-screen areas)
Rect{ -400, 0, 800, 600 }.draw();
  • The following drawing ultimately shows only the last rectangle drawn, but since it paints over itself, it totals 400 x 600 x 4 = 960,000 pixels
for (int32 i = 0; i < 4; ++i)
{
	Rect{ -400, 0, 800, 600 }.draw();
}
  • There is no way to get the cumulative pixel count painted by the program

Sample Code

# include <Siv3D.hpp>

void Main()
{
	while (System::Update())
	{
		ClearPrint();
		Print << U"Draw call count: " << Profiler::GetStat().drawCalls;
		Print << U"Triangle count: " << Profiler::GetStat().triangleCount;
	}
}

80.2 Tips for Reducing Draw Calls

  • Shape drawing and texture (text) drawing use different render settings, so alternating .draw() calls between them inhibits draw call grouping
  • Draw calls can be reduced by grouping shapes together and textures together

Method 1. Alternating Shape and Text Drawing

  • Two draw calls are issued per cell, which is inefficient
Indicator Value
Draw calls ❌ 202
Triangles 461

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Font font{ FontMethod::MSDF, 48, Typeface::Heavy };

	while (System::Update())
	{
		ClearPrint();
		Print << U"Draw call count: " << Profiler::GetStat().drawCalls;
		Print << U"Triangle count: " << Profiler::GetStat().triangleCount;

		for (int32 y = 0; y < 10; ++y)
		{
			for (int32 x = 0; x < 10; ++x)
			{
				Rect{ (x * 40), (y * 40), 38 }.draw();
				font(U"0").drawAt(20, Vec2{ (x * 40) + 20, (y * 40) + 20 }, ColorF{ 0.8 });
			}
		}
	}
}

Method 2. Grouping Shape and Text Drawing Separately

  • All Rect draws are combined into one draw call
  • All text draws are combined into one draw call
Indicator Value
Draw calls ✅ 4
Triangles 457

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Font font{ FontMethod::MSDF, 48, Typeface::Heavy };

	while (System::Update())
	{
		ClearPrint();
		Print << U"Draw call count: " << Profiler::GetStat().drawCalls;
		Print << U"Triangle count: " << Profiler::GetStat().triangleCount;

		for (int32 y = 0; y < 10; ++y)
		{
			for (int32 x = 0; x < 10; ++x)
			{
				Rect{ (x * 40), (y * 40), 38 }.draw();
			}
		}

		for (int32 y = 0; y < 10; ++y)
		{
			for (int32 x = 0; x < 10; ++x)
			{
				font(U"0").drawAt(20, Vec2{ (x * 40) + 20, (y * 40) + 20 }, ColorF{ 0.8 });
			}
		}
	}
}

80.3 Tips for Reducing Triangle Count

  • Using grid drawing as an example, let's consider ways to reduce triangle count through drawing method improvements

Method 1. Using Rect::drawFrame() for All Cells

  • Rect::drawFrame() draws 8 triangles per call
Indicator Value
Draw calls 3
Triangles ❌❌ 859

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	while (System::Update())
	{
		ClearPrint();
		Print << U"Draw call count: " << Profiler::GetStat().drawCalls;
		Print << U"Triangle count: " << Profiler::GetStat().triangleCount;

		Rect{ 400, 400 }.draw();

		for (int32 y = 0; y < 10; ++y)
		{
			for (int32 x = 0; x < 10; ++x)
			{
				Rect{ (x * 40), (y * 40), 40 }.drawFrame(1, 0, ColorF{ 0.0 });
			}
		}
	}
}

Method 2. Using Rect::draw() with Gaps

  • Rect::draw() draws 2 triangles
  • Triangle count is reduced compared to method 1, but pixel fill count doubles
Indicator Value
Draw calls 3
Triangles ❌ 259

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	while (System::Update())
	{
		ClearPrint();
		Print << U"Draw call count: " << Profiler::GetStat().drawCalls;
		Print << U"Triangle count: " << Profiler::GetStat().triangleCount;

		Rect{ 400, 400 }.draw(ColorF{ 0.0 });

		for (int32 y = 0; y < 10; ++y)
		{
			for (int32 x = 0; x < 10; ++x)
			{
				Rect{ (x * 40), (y * 40), 40 }.stretched(-1).draw();
			}
		}
	}
}

Method 3. Drawing Only Vertical and Horizontal Lines

  • By drawing only the background and vertical/horizontal lines, triangle count and pixel fill count are minimized
Indicator Value
Draw calls 3
Triangles ✅ 103

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	while (System::Update())
	{
		ClearPrint();
		Print << U"Draw call count: " << Profiler::GetStat().drawCalls;
		Print << U"Triangle count: " << Profiler::GetStat().triangleCount;

		Rect{ 400, 400 }.draw();

		for (int32 i = 0; i <= 10; ++i)
		{
			Rect{ -1, (-1 + (i * 40)), 402, 2 }.draw(ColorF{ 0.0 });
			Rect{ (-1 + (i * 40)), -1, 2, 402 }.draw(ColorF{ 0.0 });
		}
	}
}

80.4 Drawing Colored and Numbered Grids Efficiently

  • When drawing many colored Rect objects in a grid, consider using Image + Texture
  • All cells can be drawn with just one draw call and 2 triangles
  • For numbers 0 to N, drawing performance can be saved by skipping rendering when the value is 0
Indicator Value
Draw calls 5
Triangles 255

# include <Siv3D.hpp>

Color ToColor(int32 n)
{
	static const std::array<Color, 4> palettes = { Colormap01(0), Colormap01(0.33), Colormap01(0.67), Colormap01(1.0) };
	return palettes[n];
}

void UpdateImageFromGrid(const Grid<int32>& grid, Image& image)
{
	assert(grid.size() == image.size());

	for (int32 y = 0; y < grid.height(); ++y)
	{
		for (int32 x = 0; x < grid.width(); ++x)
		{
			image[y][x] = ToColor(grid[y][x]);
		}
	}
}

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Font font{ FontMethod::MSDF, 48, Typeface::Heavy };
	const TextStyle textStyle = TextStyle::OutlineShadow(0.2, ColorF{ 0.1 }, Vec2{ 2, 2 }, ColorF{ 0.1 });

	Grid<int32> grid(10, 10, 0);
	for (size_t y = 0; y < grid.height(); ++y)
	{
		for (size_t x = 0; x < grid.width(); ++x)
		{
			grid[y][x] = Random(0, 3);
		}
	}

	Image image{ grid.size() };
	UpdateImageFromGrid(grid, image);
	DynamicTexture texture{ image };

	while (System::Update())
	{
		ClearPrint();
		Print << U"Draw call count: " << Profiler::GetStat().drawCalls;
		Print << U"Triangle count: " << Profiler::GetStat().triangleCount;

		/*
		if (grid was updated)
		{
			UpdateImageFromGrid(grid, image);
			texture.fill(image);
		}
		*/

		{
			const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
			texture.resized(grid.size() * 40).draw();
		}

		for (size_t i = 0; i <= grid.width(); ++i)
		{
			Rect{ -1, (-1 + (i * 40)), (grid.width() * 40 + 2), 2}.draw();
			Rect{ (-1 + (i * 40)), -1, 2, (grid.height() * 40 + 2) }.draw();
		}

		for (int32 y = 0; y < grid.height(); ++y)
		{
			for (int32 x = 0; x < grid.width(); ++x)
			{
				if (const int32 n = grid[y][x])
				{
					const Vec2 pos{ (x * 40 + 20), (y * 40 + 20) };
					font(n).drawAt(textStyle, 25, pos);
				}
			}
		}
	}
}

80.5 Drawing a 256x256 Grid

  • Based on 80.4, this uses Transformer2D to apply appropriate scaling while drawing a 256 x 256 cell grid to the screen
  • Text is omitted since cells are small
  • This achieves very lightweight rendering for the amount of information displayed
Indicator Value
Draw calls 4
Triangles 1089

# include <Siv3D.hpp>

Color ToColor(int32 n)
{
	static const std::array<Color, 4> palettes = { Colormap01(0), Colormap01(0.33), Colormap01(0.67), Colormap01(1.0) };
	return palettes[n];
}

void UpdateImageFromGrid(const Grid<int32>& grid, Image& image)
{
	assert(grid.size() == image.size());

	for (int32 y = 0; y < grid.height(); ++y)
	{
		for (int32 x = 0; x < grid.width(); ++x)
		{
			image[y][x] = ToColor(grid[y][x]);
		}
	}
}

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	Grid<int32> grid(256, 256, 0);
	for (size_t y = 0; y < grid.height(); ++y)
	{
		for (size_t x = 0; x < grid.width(); ++x)
		{
			grid[y][x] = Random(0, 3);
		}
	}

	Image image{ grid.size() };
	UpdateImageFromGrid(grid, image);
	DynamicTexture texture{ image };

	while (System::Update())
	{
		ClearPrint();
		Print << U"Draw call count: " << Profiler::GetStat().drawCalls;
		Print << U"Triangle count: " << Profiler::GetStat().triangleCount;

		/*
		if (grid was updated)
		{
			UpdateImageFromGrid(grid, image);
			texture.fill(image);
		}
		*/

		{
			// Apply 0.07x scaling to drawing coordinates	
			const Transformer2D tr{ Mat3x2::Scale(0.07) };

			{
				const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
				texture.resized(grid.size() * 40).draw();
			}

			for (size_t i = 0; i <= grid.width(); ++i)
			{
				Rect{ -1, (-1 + (i * 40)), (grid.width() * 40 + 2), 2 }.draw();
				Rect{ (-1 + (i * 40)), -1, 2, (grid.height() * 40 + 2) }.draw();
			}
		}
	}
}