Skip to content

Commit f10e698

Browse files
committed
Optimized string writing to use fixed-size buffer
1 parent 5d1a342 commit f10e698

5 files changed

Lines changed: 178 additions & 11 deletions

File tree

devlog.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
No new features for you! This time developing was dedicated almost entirely to refactoring our render pipeline so it's easier to use.
2+
3+
A graphics engine is pretty general-use, for once I have one up and running, I'm free to do essentially whatever I feel like. The problem is the quick graphics system I built yesterday is kind of bad.
4+
When I was writing my last devlog, I wanted to make a quick demo scene to show the rendering engine up and running. In doing so, I realized that the way I had structured my data pipeline was kind of all over the place, and required me to keep track of a bunch different buffers, that I was cloning and moving every draw frame. I took a quick look at my performance graph, and found that 6% of usage was in the `rasterize` function, which allocated memory every time a new frame was called.
5+
6+
We can't be having that. So, I decided to refactor my system to use a `Mesh` structure that holds three distinct buffers: our vertices, those vertices after being transformed, and them after being rasterized. This way, I allocate memory once, and modify these buffers at runtime, whenever needed.
7+
This required to rewrite _every_ step of my rendering pipeline, including most of our matrix math, to mutate instead of copy and return. This is particularly annoying, because javascript passes object by reference (good!) but you can't reassign them (bad), and it doesn't throw a warning if you try to.
8+
9+
Anyways, everything is now done! This leaves us free to try to further optimize the `<svg>`, or pivot into actually doing anything with our engine.
10+
Attached, the new performance results!
11+
12+
Obs: I thought this would be a quick refactor, and didn't plan ahead. No issues nor modularization of commits in this one :\(
13+
14+
**Commits**
15+
[Commit 5d1a342](https://github.com/Sekqies/native-html-images/commit/5d1a34267f30762c0e1550e14240dc450b4d9bc5): Refactor to use mesh system

src/main.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { mat4, vec3 } from "./math/types";
22
import { perspective, identity, look_at, rotate, translate } from "./math/transformations";
33
import { create_sphere } from "./rendering/primitives";
44
import { build_mesh } from "./rendering/render";
5+
import { StringBuffer } from "./utils/string_buffer";
56

67

78
export function main_3d() {
@@ -15,18 +16,19 @@ export function main_3d() {
1516
const view:mat4 = look_at(vec3(0,2,6.5),vec3(0,0,0),y);
1617
const projection = perspective(60 * Math.PI / 180, 400/300, 0.1, 100);
1718
let time = 0;
19+
const string_buffer = new StringBuffer(1024*1024);
1820
const loop = () => {
1921
time += 0.01;
2022

2123
let frame_html = "";
2224
let sun_model = identity();
2325
sun_model = rotate(sun_model, time * 0.5, y);
24-
frame_html += build_mesh(sun_mesh, sun_model,view,projection,do_wireframe,true);
26+
frame_html += build_mesh(sun_mesh, sun_model,view,projection,string_buffer,do_wireframe,true);
2527
let planet_model = identity();
2628
planet_model = rotate(planet_model, time, vec3(0, 1, 0));
2729
planet_model = translate(planet_model, vec3(3.5, 0, 0));
2830
planet_model = rotate(planet_model, time * 3, vec3(1, 0, 1));
29-
frame_html += build_mesh(planet_mesh, planet_model,view,projection,do_wireframe,true);
31+
frame_html += build_mesh(planet_mesh, planet_model,view,projection,string_buffer,do_wireframe,true);
3032
target!.innerHTML = frame_html;
3133
requestAnimationFrame(loop);
3234
}

src/rendering/render.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { mul_mat4 } from "../math/matrix_operators";
55
import type { mat4 } from "../math/types";
66
import { transform_vertices } from "./vertex";
77
import { assemble_primitives } from "./primitive_assembler";
8+
import type { StringBuffer } from "../utils/string_buffer";
89

910
export function render(mesh: Mesh, model:mat4, view:mat4, projection:mat4, invert_y:boolean = true, stride:number = 4):void {
1011
const mvp = mul_mat4(mul_mat4(projection,view),model);
@@ -24,9 +25,9 @@ export function render(mesh: Mesh, model:mat4, view:mat4, projection:mat4, inver
2425
* @param stride
2526
* @returns An html string for the rendered mesh. This is to be included inside an <svg> tag, with a ViewBox attribute.
2627
*/
27-
export function build_mesh(mesh:Mesh,model:mat4,view:mat4,projection:mat4, use_rect:boolean = true, invert_y: boolean = true, stride:number = 4):string{
28+
export function build_mesh(mesh:Mesh,model:mat4,view:mat4,projection:mat4, string_buffer:StringBuffer, use_rect:boolean = true, invert_y: boolean = true, stride:number = 4):string{
2829
render(mesh,model,view,projection,invert_y,stride);
29-
const svg_content = build_3d_svg(mesh.raster_buffer, mesh.raster_end, use_rect);
30+
const svg_content = build_3d_svg(mesh.raster_buffer, mesh.raster_end, use_rect,string_buffer);
3031
return svg_content;
3132
}
3233

src/to_html.ts

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ArrayType } from "./math/types";
2+
import { character_to_uint, SB_TOKENS, string_to_uint, type StringBuffer } from "./utils/string_buffer";
23

34
export type vec3 = [number,number,number];
45

@@ -71,9 +72,60 @@ function get_rect_edge(x1:number, y1:number,x2:number,y2:number,thickness:number
7172
return `<rect x="0" y="${-thickness / 2}" width="${len}" height="${thickness}" transform="translate(${x1} ${y1}) rotate(${ang})"/>`;
7273
}
7374

74-
export function build_3d_svg(vertices:ArrayType, end:number, use_rect:boolean):string{
75+
const RECT_TOKENS = {
76+
HEAD: string_to_uint('<rect x="0" y="'),
77+
WIDTH: string_to_uint('" width="'),
78+
HEIGHT: string_to_uint('" height="'),
79+
TRANSFORM: string_to_uint('" transform="translate('),
80+
ROTATE: string_to_uint(') rotate('),
81+
TAIL: string_to_uint(')"/>')
82+
};
83+
84+
function get_rect_edge_buffer(x1:number, y1:number,x2:number,y2:number,thickness:number, buffer:StringBuffer){
85+
const dx = x2 - x1;
86+
const dy = y2 - y1;
87+
const len = Math.sqrt(dx * dx + dy * dy);
88+
const ang = Math.atan2(dy, dx) * (180 / Math.PI);
89+
const y_offset = -thickness / 2;
90+
buffer.write_chunk(RECT_TOKENS.HEAD);
91+
buffer.write_float(y_offset);
92+
93+
buffer.write_chunk(RECT_TOKENS.WIDTH);
94+
buffer.write_float(len);
95+
96+
buffer.write_chunk(RECT_TOKENS.HEIGHT);
97+
buffer.write_float(thickness);
98+
99+
buffer.write_chunk(RECT_TOKENS.TRANSFORM);
100+
buffer.write_float(x1);
101+
buffer.push(32);
102+
buffer.write_float(y1);
103+
104+
buffer.write_chunk(RECT_TOKENS.ROTATE);
105+
buffer.write_float(ang);
106+
107+
buffer.write_chunk(RECT_TOKENS.TAIL);
108+
}
109+
110+
const POLYGON_TOKENS = {
111+
HEAD: string_to_uint('<polygon points = "'),
112+
TAIL: string_to_uint('"/>')
113+
}
114+
115+
function push_pair(x:number,y:number,buffer:StringBuffer){
116+
buffer.write_float(x);
117+
buffer.push(SB_TOKENS.COMMA);
118+
buffer.write_float(y);
119+
buffer.push(SB_TOKENS.SPACE);
120+
}
121+
122+
123+
const decoder = new TextDecoder('ascii');
124+
125+
export function build_3d_svg(vertices:ArrayType, end:number, use_rect:boolean, buffer:StringBuffer):string{
75126
const n = end;
76-
let html = "";
127+
buffer.reset();
128+
77129

78130
const thickness = 0.005;
79131

@@ -90,14 +142,18 @@ export function build_3d_svg(vertices:ArrayType, end:number, use_rect:boolean):s
90142

91143

92144
if(use_rect){
93-
html+= get_rect_edge(x1,y1,x2,y2,thickness);
94-
html+= get_rect_edge(x2,y2,x3,y3,thickness);
95-
html+= get_rect_edge(x3,y3,x1,y1,thickness);
145+
get_rect_edge_buffer(x1,y1,x2,y2,thickness,buffer);
146+
get_rect_edge_buffer(x2,y2,x3,y3,thickness,buffer);
147+
get_rect_edge_buffer(x3,y3,x1,y1,thickness,buffer);
96148
continue;
97149
}
98-
html += `<polygon points = "${x1},${y1} ${x2},${y2} ${x3},${y3}"/>`;
150+
buffer.write_chunk(POLYGON_TOKENS.HEAD);
151+
push_pair(x1,y1,buffer);
152+
push_pair(x2,y2,buffer);
153+
push_pair(x3,y3,buffer);
154+
buffer.write_chunk(POLYGON_TOKENS.TAIL);
99155
}
100-
return html;
156+
return decoder.decode(buffer.buffer.subarray(0,buffer.cursor));
101157
}
102158

103159

src/utils/string_buffer.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
2+
export function string_to_uint(str: string): Uint8Array {
3+
const arr = new Uint8Array(str.length);
4+
for (let i = 0; i < str.length; i++) {
5+
arr[i] = str.charCodeAt(i);
6+
}
7+
return arr;
8+
}
9+
10+
export function character_to_uint(str:string):number{
11+
return str.charCodeAt(0);
12+
}
13+
14+
15+
const MINUS = character_to_uint("-");
16+
const ZERO = character_to_uint("0");
17+
const DOT = character_to_uint(".");
18+
19+
20+
export const SB_TOKENS = {
21+
MINUS: MINUS,
22+
ZERO: ZERO,
23+
DOT: DOT,
24+
COMMA: character_to_uint(','),
25+
SPACE: character_to_uint(' ')
26+
}
27+
28+
export class StringBuffer{
29+
cursor:number;
30+
buffer:Uint8Array;
31+
constructor(size:number){
32+
this.buffer = new Uint8Array(size);
33+
this.cursor = 0;
34+
}
35+
36+
push(val:number){
37+
this.buffer[this.cursor++] = val;
38+
}
39+
40+
write_chunk(chunk:Uint8Array):void {
41+
this.buffer.set(chunk,this.cursor);
42+
this.cursor += chunk.length;
43+
}
44+
45+
write_float(val:number):void {
46+
if(val < 0){
47+
this.push(MINUS);
48+
val = -val;
49+
}
50+
const fixed_point = (val * 1000 + 0.5) | 0;
51+
const integer_part = (fixed_point / 1000) | 0;
52+
53+
if(integer_part !== 0){
54+
const start = this.cursor;
55+
let temp = integer_part;
56+
while(temp > 0){
57+
const digit = temp%10;
58+
this.push(ZERO + digit);
59+
temp = (temp/10) | 0;
60+
}
61+
let left = start;
62+
let right = this.cursor - 1;
63+
while(left<right){
64+
const swap = this.buffer[left];
65+
this.buffer[left++] = this.buffer[right];
66+
this.buffer[right--] = swap;
67+
}
68+
}
69+
else {
70+
this.push(ZERO);
71+
}
72+
this.push(DOT);
73+
let decimal_part = fixed_point % 1000;
74+
const first_digit = (decimal_part / 100) | 0;
75+
this.push(ZERO + first_digit);
76+
decimal_part %= 100;
77+
78+
const second_digit = (decimal_part / 10) | 0;
79+
this.push(ZERO + second_digit);
80+
const third_digit = decimal_part % 10;
81+
this.push(ZERO + third_digit);
82+
}
83+
84+
write_string(str:string):void {
85+
for(let i = 0; i < str.length; ++i){
86+
this.push(str.charCodeAt(i));
87+
}
88+
}
89+
90+
reset():void {
91+
this.cursor = 0;
92+
}
93+
}

0 commit comments

Comments
 (0)