Skip to content

Commit d482ff6

Browse files
authored
Merge pull request #109 from obiwac/ep13b
Episode 13b: Performance improvements
2 parents 405eac5 + 6e5369c commit d482ff6

16 files changed

Lines changed: 816 additions & 364 deletions

File tree

.github/workflows/ep13-lsp.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ jobs:
1414
with:
1515
python-version: "3.11"
1616
- uses: abatilo/actions-poetry@v2
17-
- name: Install dependencies
18-
run: poetry install --no-root --with dev
17+
- name: Install dependencies and build Cython extension
18+
run: poetry install --no-root
19+
- name: Rebuild the project
20+
run: poetry build
1921
- name: Cache venv created by poetry (configured to be in '.venv')
2022
uses: actions/cache@v3
2123
with:
2224
path: ./.venv
2325
key: venv-${{ runner.os }}-${{ hashFiles('poetry.lock') }}
2426
- name: Run pyright
25-
run: |
26-
poetry run pyright
27+
run: poetry run pyright

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,12 @@
44
.vscode
55
*.ogg
66
*.sw[nop]
7+
*.so
8+
*.c
9+
*.so
10+
*.html
11+
*.cache
12+
dist
13+
build
14+
*.egg-info
15+
*.prof

episode-13/README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,27 @@ Clock on the thumbnail below to watch the video:
77
This episode is split into two parts:
88

99
- EP13a: The code has gotten a little crusty. This episode restructures the codebase, adds a `pyproject.toml` file and switches to Poetry for better dependency management, and adds a formatter and a linter as well as a CI setup to check all this.
10-
- EP13b: Loading big save files took a long-ass time before this. This episode covers profiling and optimization techniques and rewrites a lot of the chunk loading code in Cython to speed it up dramatically.
10+
- EP13b: Loading big save files took a long-ass time before this. This episode covers profiling and optimization techniques and rewrites a lot of the chunk loading code in Cython to speed it up dramatically. It also covers some frame time improvements in preparation for adding mobs.
11+
12+
## Installing dependencies
13+
14+
Using [Poetry](https://python-poetry.org/):
15+
16+
```console
17+
poetry install
18+
```
19+
20+
This will also build the Cython extensions.
21+
If you ever need to rebuild them, you can do:
22+
23+
```console
24+
poetry build
25+
```
26+
27+
## Running
28+
29+
Again, using Poetry:
30+
31+
```console
32+
poetry run python mcpy.py
33+
```

episode-13/build.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from setuptools.command.build_ext import build_ext
2+
from Cython.Build import cythonize
3+
import Cython.Compiler.Options
4+
5+
Cython.Compiler.Options.cimport_from_pyx = True # needed?
6+
7+
8+
class BuildExt(build_ext):
9+
def build_extension(self, ext):
10+
self.inplace = True # Important or the LSP won't have access to the compiled files.
11+
super().build_extension(ext)
12+
13+
14+
def build(setup_kwargs):
15+
ext_modules = cythonize(
16+
[
17+
"src/chunk/__init__.pyx",
18+
"src/chunk/chunk.pyx",
19+
"src/chunk/subchunk.pyx",
20+
],
21+
compiler_directives={
22+
"language_level": 3,
23+
"profile": True,
24+
},
25+
annotate=True,
26+
)
27+
28+
setup_kwargs.update({"ext_modules": ext_modules, "cmdclass": {"build_ext": BuildExt}})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import math
22
import random
3+
from cProfile import Profile
34
import pyglet
45

56
pyglet.options["shadow_window"] = False
@@ -50,6 +51,32 @@ def update(self, delta_time):
5051

5152
self.player.update(delta_time)
5253

54+
# Load the closest chunk which hasn't been loaded yet.
55+
56+
x, y, z = self.player.position
57+
closest_chunk = None
58+
min_distance = math.inf
59+
60+
for chunk_pos, chunk in self.world.chunks.items():
61+
if chunk.loaded:
62+
continue
63+
64+
cx, cy, cz = chunk_pos
65+
66+
cx *= CHUNK_WIDTH
67+
cy *= CHUNK_HEIGHT
68+
cz *= CHUNK_LENGTH
69+
70+
dist = (cx - x) ** 2 + (cy - y) ** 2 + (cz - z) ** 2
71+
72+
if dist < min_distance:
73+
min_distance = dist
74+
closest_chunk = chunk
75+
76+
if closest_chunk is not None:
77+
closest_chunk.update_subchunk_meshes()
78+
closest_chunk.update_mesh()
79+
5380
def on_draw(self):
5481
self.player.update_matrices()
5582

@@ -220,6 +247,38 @@ def run(self):
220247
pyglet.app.run()
221248

222249

250+
def sample_initial_loading_time():
251+
with Profile() as profiler:
252+
Game()
253+
profiler.create_stats()
254+
profiler.dump_stats("stats.prof")
255+
256+
for k in profiler.stats.keys():
257+
# file line name
258+
file, _, name = k
259+
260+
if "world.py" in file and name == "__init__":
261+
# ? ncalls time cumtime parent
262+
_, _, _, cumtime, _ = profiler.stats[k]
263+
break
264+
265+
else:
266+
raise Exception("Couldn't find work init stats!")
267+
268+
return cumtime
269+
270+
271+
def benchmark_initial_loading_time():
272+
n = 10
273+
samples = [sample_initial_loading_time() for _ in range(n)]
274+
mean = sum(samples) / n
275+
276+
print(mean)
277+
exit()
278+
279+
223280
if __name__ == "__main__":
281+
# benchmark_initial_loading_time()
282+
224283
game = Game()
225284
game.run()

episode-13/poetry.lock

Lines changed: 190 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

episode-13/pyproject.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,24 @@ authors = [
1414
]
1515
readme = "README.md"
1616

17+
[build-system]
18+
requires = ["poetry-core", "setuptools", "cython"]
19+
20+
[tool.poetry.build]
21+
script = "build.py"
22+
generate-setup-file = true
23+
1724
[tool.poetry.dependencies]
1825
python = "^3.10"
1926
pyglet = "^2.0.16"
2027
nbtlib = "^2.0.4"
2128
base36 = "^0.1.1"
29+
pyglm = "^2.7.1"
2230

2331
[tool.poetry.group.dev.dependencies]
2432
ruff = "^0.5.5"
2533
pyright = "^1.1.374"
34+
cython = "^3.0.11"
2635

2736
[tool.pyright]
2837
exclude = [".venv"]
@@ -34,6 +43,6 @@ venv = ".venv"
3443
reportIndexIssue = false
3544

3645
# From https://github.com/obiwac/python-minecraft-clone/pull/107:
37-
# F405 * may be undefined, or defined from star imports: These are indeed defined from star imports. I guess we could import all the symbols in '__all__' explicitly, but if there's a mistake here and it causes a runtime error, that's not the end of the world.
46+
# F405 X may be undefined, or defined from star imports: These are indeed defined from star imports. I guess we could import all the symbols in '__all__' explicitly, but if there's a mistake here and it causes a runtime error, that's not the end of the world.
3847

3948
ignore = ["models/__init__.py"]

episode-13/src/chunk/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from src.chunk.common import CHUNK_HEIGHT, CHUNK_LENGTH, CHUNK_WIDTH
2+
from src.chunk.common import SUBCHUNK_HEIGHT, SUBCHUNK_LENGTH, SUBCHUNK_WIDTH
3+
4+
__all__ = [
5+
"CHUNK_WIDTH",
6+
"CHUNK_HEIGHT",
7+
"CHUNK_LENGTH",
8+
"SUBCHUNK_WIDTH",
9+
"SUBCHUNK_HEIGHT",
10+
"SUBCHUNK_LENGTH",
11+
]

episode-13/src/chunk/__init__.pyx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from src.chunk.common import CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_LENGTH
2+
from src.chunk.common import SUBCHUNK_WIDTH, SUBCHUNK_HEIGHT, SUBCHUNK_LENGTH
3+
4+
from libc.stdlib cimport malloc, free
5+
from libc.string cimport memset
6+
from libc.stdint cimport uint8_t, uint32_t
7+
8+
cdef int C_CHUNK_WIDTH = CHUNK_WIDTH
9+
cdef int C_CHUNK_HEIGHT = CHUNK_HEIGHT
10+
cdef int C_CHUNK_LENGTH = CHUNK_LENGTH
11+
12+
cdef int C_SUBCHUNK_WIDTH = SUBCHUNK_WIDTH
13+
cdef int C_SUBCHUNK_HEIGHT = SUBCHUNK_HEIGHT
14+
cdef int C_SUBCHUNK_LENGTH = SUBCHUNK_LENGTH
15+
16+
cdef class CSubchunk:
17+
cdef size_t data_count
18+
cdef float* data
19+
20+
cdef size_t index_count
21+
cdef uint32_t* indices
22+
23+
def __init__(self):
24+
self.data_count = 0
25+
self.index_count = 0
26+
27+
cdef class CChunk:
28+
cdef size_t data_count
29+
cdef float* data
30+
31+
cdef size_t index_count
32+
cdef int* indices
33+
34+
cdef size_t size
35+
cdef uint8_t* blocks
36+
37+
def __init__(self):
38+
self.size = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_LENGTH * sizeof(self.blocks[0])
39+
self.blocks = <uint8_t*>malloc(self.size)
40+
memset(self.blocks, 0, self.size)
41+
42+
def __del__(self):
43+
free(self.blocks)
44+
45+
@property
46+
def index_count(self):
47+
return self.index_count
48+
49+
def get_blocks(self, i):
50+
return self.blocks[i]
51+
52+
def set_blocks(self, i, val):
53+
self.blocks[i] = val
54+
55+
def copy_blocks(self, blocks):
56+
cdef int i
57+
cdef int length = len(blocks)
58+
59+
for i in range(length):
60+
self.blocks[i] = blocks[i]

0 commit comments

Comments
 (0)