Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org).
## [UNRELEASED]
### Added
- Import npz files for multiple frames or sprites.
- Created large, block letters that can be rendered in the terminal and included a utility to generate, extend, and export them.
- Demo/utility to render an image, GIF, or video in the terminal.

### Changed
- Refactor `HemeraTermFx` to further optimize terminal printing performance.

### Removed
- Removed old example files and demos.
---

## [0.1.0-alpha] - 2024-12-27
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
![NyxEngine Logo](docs/readme_assets/nyx-logo-horizontal.png)

# NyxEngine

An experimental, high-performance game engine and rendering pipeline for the terminal. Written in Python with NumPy.

[View the NyxEngine project webpage!](https://cmorman89.github.io/nyx-engine)

---

## Status Update:
Expand Down
Binary file not shown.
Binary file removed docs/readme_assets/animated_stars.gif
Binary file not shown.
Binary file removed docs/readme_assets/doom_small.mp4
Binary file not shown.
Binary file removed docs/readme_assets/nyx_moon.png
Binary file not shown.
Binary file removed docs/readme_assets/optimization_before_after.mp4
Binary file not shown.
Binary file removed docs/readme_assets/spaceship-lasers.gif
Binary file not shown.
314 changes: 42 additions & 272 deletions docs/working_notes.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -176,284 +176,54 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[1 0 0 0 0 0 0 0 0 0]\n",
" [0 1 0 0 0 0 0 0 0 0]\n",
" [0 0 1 0 0 0 0 0 0 0]\n",
" [0 0 0 1 0 0 0 0 0 0]\n",
" [0 0 0 0 1 0 0 0 0 0]\n",
" [0 0 0 0 0 1 0 0 0 0]\n",
" [0 0 0 0 0 0 1 0 0 0]\n",
" [0 0 0 0 0 0 0 1 0 0]\n",
" [0 0 0 0 0 0 0 0 1 0]\n",
" [0 0 0 0 0 0 0 0 0 1]]\n",
"[[0 1 1 1 1 1 1 1 1 1]\n",
" [1 0 1 1 1 1 1 1 1 1]\n",
" [1 1 0 1 1 1 1 1 1 1]\n",
" [1 1 1 0 1 1 1 1 1 1]\n",
" [1 1 1 1 0 1 1 1 1 1]\n",
" [1 1 1 1 1 0 1 1 1 1]\n",
" [1 1 1 1 1 1 0 1 1 1]\n",
" [1 1 1 1 1 1 1 0 1 1]\n",
" [1 1 1 1 1 1 1 1 0 1]\n",
" [1 1 1 1 1 1 1 1 1 0]]\n"
]
}
],
"source": [
"import numpy as np\n",
"\n",
"new_subpixel_frame = np.ones((10, 10), dtype=np.uint8)\n",
"old_frame = np.eye(10, dtype=np.uint8)\n",
"delta_buffer = np.where(new_subpixel_frame != old_frame, new_subpixel_frame, 0)\n",
"\n",
"print(old_frame)\n",
"print(delta_buffer)\n"
]
},
{
"cell_type": "code",
"execution_count": 93,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[[ 0 1 2 3 4]\n",
" [10 0 12 13 14]\n",
" [20 21 0 23 24]\n",
" [30 31 32 0 34]\n",
" [40 41 42 43 0]]\n",
"\n",
" [[ 0 6 7 8 9]\n",
" [15 0 17 18 19]\n",
" [25 26 0 28 29]\n",
" [35 36 37 0 39]\n",
" [45 46 47 48 0]]]\n",
"Changed = False\n"
]
}
],
"source": [
"# Subpixel and Delta testing\n",
"import numpy as np\n",
"\n",
"old_frame = np.array([\n",
" [0 for _ in range(5)], # fg color\n",
" [5 for _ in range(5)], # bg color\n",
" [11 for _ in range(5)], # fg color\n",
" [16 for _ in range(5)], # bg color\n",
" [22 for _ in range(5)], # fg color\n",
" [27 for _ in range(5)], # bg color\n",
" [33 for _ in range(5)], # fg color\n",
" [38 for _ in range(5)], # bg color\n",
" [44 for _ in range(5)], # fg color\n",
" [49 for _ in range(5)], # bg color\n",
"], dtype=np.uint8)\n",
"\n",
"new_frame = np.arange(50).reshape(10, 5) # Mock frame\n",
"\n",
"\n",
"old_subpixel_frame = np.stack([old_frame[::2, :], old_frame[1::2, :]], axis=1) # split fg and bg to make 3d array\n",
"new_subpixel_frame = np.stack([new_frame[::2, :], new_frame[1::2, :]], axis=1) # split fg and bg to make 3d array\n",
"\n",
"# Keep new_subpixel_frame fg & bg pixel at a given x, y index if EITHER fg OR bg color had changed from the same z-pair at the same x, y coordinates in the old_subpixel_frame\n",
"# If both pairs are the same, replace with zeros\n",
"\n",
"delta_frame = np.where(\n",
" np.any(new_subpixel_frame != old_subpixel_frame, axis=1, keepdims=True),\n",
" new_subpixel_frame,\n",
" np.zeros_like(new_subpixel_frame)\n",
")\n",
"print(delta_subpixel_frame)\n",
"\n",
"\n",
"# Mock slice:\n",
"old_frame_pair = np.array([16, 17])\n",
"\n",
"# Get subpixel pair = ([[16, 17]])\n",
"new_frame_pair = new_subpixel_frame[1:2:, 1:2:, 1:3] \n",
"\n",
"# Compare. Returns True if ANY change\n",
"changed = np.any(new_frame_pair != old_frame_pair)\n",
"print(f\"Changed = {changed}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from datetime import datetime\n",
"from typing import List\n",
"from multiprocessing import Pool, cpu_count\n",
"\n",
"# A CPU-bound task (e.g., squaring a large range of numbers)\n",
"def square_number(n):\n",
" return n * n\n",
"\n",
"if __name__ == \"__main__\":\n",
" # Input data\n",
" numbers: List(int) = list(range(1, 100000000))\n",
" start = datetime.now()\n",
" # Create a pool of workers equal to the number of CPU cores\n",
" with Pool(cpu_count()) as pool:\n",
" # Map the function to the data\n",
" results = pool.map(square_number, numbers)\n",
" stop = datetime.now()\n",
" delta = stop - start\n",
" print(delta)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'List' is not defined",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[0;32mIn[1], line 8\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m n\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;18m__name__\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__main__\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 7\u001b[0m \u001b[38;5;66;03m# Input data\u001b[39;00m\n\u001b[0;32m----> 8\u001b[0m numbers: \u001b[43mList\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(\u001b[38;5;28mrange\u001b[39m(\u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m100000000\u001b[39m))\n\u001b[1;32m 9\u001b[0m start \u001b[38;5;241m=\u001b[39m datetime\u001b[38;5;241m.\u001b[39mnow()\n\u001b[1;32m 10\u001b[0m results \u001b[38;5;241m=\u001b[39m [square_number(number) \u001b[38;5;28;01mfor\u001b[39;00m number \u001b[38;5;129;01min\u001b[39;00m numbers]\n",
"\u001b[0;31mNameError\u001b[0m: name 'List' is not defined"
]
}
],
"source": [
"from datetime import datetime\n",
"# A CPU-bound task (e.g., squaring a large range of numbers)\n",
"def square_number(n):\n",
" return n * n\n",
"\n",
"if __name__ == \"__main__\":\n",
" # Input data\n",
" numbers: List(int) = list(range(1, 100000000))\n",
" start = datetime.now()\n",
" results = [square_number(number) for number in numbers]\n",
" stop = datetime.now()\n",
" delta = stop - start\n",
" print(delta)"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Sequential execution time: 0:00:09.466620\n"
]
}
],
"cell_type": "markdown",
"metadata": {
"tags": [
"parameters"
]
},
"source": [
"from datetime import datetime\n",
"\n",
"# Function to check if a point (c) is in the Mandelbrot set\n",
"def mandelbrot(c, max_iter=1000):\n",
" z = 0 + 0j\n",
" for n in range(max_iter):\n",
" z = z * z + c\n",
" if abs(z) > 2:\n",
" return n\n",
" return max_iter\n",
"\n",
"if __name__ == \"__main__\":\n",
" # Define the dimensions of the grid and the range of complex numbers\n",
" width, height = 800, 800\n",
" x_min, x_max = -2.0, 1.0\n",
" y_min, y_max = -1.5, 1.5\n",
" max_iter = 1000\n",
"\n",
" # Start timer\n",
" start = datetime.now()\n",
"\n",
" # Sequential calculation of the Mandelbrot set\n",
" result = []\n",
" for i in range(height):\n",
" row = []\n",
" for j in range(width):\n",
" x = x_min + (x_max - x_min) * j / (width - 1)\n",
" y = y_min + (y_max - y_min) * i / (height - 1)\n",
" c = complex(x, y)\n",
" row.append(mandelbrot(c, max_iter))\n",
" result.append(row)\n",
"\n",
" # Stop timer\n",
" stop = datetime.now()\n",
" delta = stop - start\n",
" print(f\"Sequential execution time: {delta}\")\n"
"### Intermediate Goal: Terminal Printing Optimization\n",
"---\n",
"- **Goal**\n",
" - [x] Use line profiling to identify bottlenecks in the rendering pipeline.\n",
" - [x] Aggresively optimize the printing pipeline. \n",
"\n",
"- **Results**\n",
" \n",
" | Version | Total Time | Per Frame Time | FPS |\n",
" |--------|--------|--------|--------|\n",
" | **v0.1.1-alpha** | **4.64321 s** | **0.0141 s** | **70.9** |\n",
" | v0.1.0-alpha | 10.1012 s | 0.0307 s | 32.6 |\n",
" | v0.0.4-alpha | 40.4568 s | 0.1230 s | 8.1 |\n",
"\n",
"- **Notes**\n",
" - Unexpectedly, the bottleneck was not in the actual terminal printing, but in the string buffer generation.\n",
" - Optimization has taken the time to render 329 frames from 40.4568 seconds to 4.64321 seconds, a 88.5% reduction in time or a **10.9x** speedup in frame printing.\n",
" - Removed tuple generation within the loop\n",
" - Removed string concatenation within the loop\n",
" - Used a sum function to combine fg and bg color ints to check for changes in both colors while only fetching one value.\n",
" - The summed pixels are then summed into row sums, which are used to skip rows that have not changed.\n",
" - Split the sum array and the fg color arrays into separate arrays to allow for different data types.\n",
" - Use StringIO to buffer the string after accumulating the row strings.\n",
" - Only fetch fg color if first the row and then the pixel has changed.\n",
" - Use the sum and the fg color to compute the original bg color without having to fetch it from the array/memory.\n",
" - Ensured all variables are preallocated and precomputed before the loop, or at least instantiated outside of the loop.\n",
" - Generate and store two dictionaries at runtime to map the fg and bg colors to their respective full ANSI escape strings instead of generating them within the loop.\n"
]
},
{
"cell_type": "code",
"execution_count": 20,
"cell_type": "markdown",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Parallel execution time: 0:00:01.671719\n"
]
}
],
"source": [
"from datetime import datetime\n",
"from multiprocessing import Pool\n",
"\n",
"# Function to check if a point (c) is in the Mandelbrot set\n",
"def mandelbrot(c, max_iter=1000):\n",
" z = 0 + 0j\n",
" for n in range(max_iter):\n",
" z = z * z + c\n",
" if abs(z) > 2:\n",
" return n\n",
" return max_iter\n",
"\n",
"# Function to calculate a row of the Mandelbrot set\n",
"def calculate_row(i, width, x_min, x_max, y_min, y_max, max_iter):\n",
" row = []\n",
" for j in range(width):\n",
" x = x_min + (x_max - x_min) * j / (width - 1)\n",
" y = y_min + (y_max - y_min) * i / (height - 1)\n",
" c = complex(x, y)\n",
" row.append(mandelbrot(c, max_iter))\n",
" return row\n",
"\n",
"if __name__ == \"__main__\":\n",
" # Define the dimensions of the grid and the range of complex numbers\n",
" width, height = 800, 800\n",
" x_min, x_max = -2.0, 1.0\n",
" y_min, y_max = -1.5, 1.5\n",
" max_iter = 1000\n",
"\n",
" # Start timer\n",
" start = datetime.now()\n",
"\n",
" # Create a pool of workers\n",
" with Pool() as pool:\n",
" # Parallel calculation of the Mandelbrot set, distributing rows across workers\n",
" result = pool.starmap(calculate_row, [(i, width, x_min, x_max, y_min, y_max, max_iter) for i in range(height)])\n",
"\n",
" # Stop timer\n",
" stop = datetime.now()\n",
" delta = stop - start\n",
" print(f\"Parallel execution time: {delta}\")\n"
"### Intermediate Goal: Working Demo\n",
"---\n",
"- **Goal**\n",
" - [x] Create a working demo of the rendering/printing pipeline.\n",
" - [x] Allow for the easy printing of images, gifs, and videos to the terminal.\n",
" - [x] Import NPZ files for easy image loading.\n",
" - [ ] Create an easy conversion tool for images to NPZ files.\n",
" - [x] Create block letters for the terminal when zoomed out and the font is too small to read otherwise."
]
}
],
Expand Down
Binary file added examples/assets/nyx/block_chars/block_chars.npz
Binary file not shown.
Loading
Loading