1 | import gg |
2 | import gx |
3 | import runtime |
4 | import time |
5 | |
6 | const pwidth = 800 |
7 | |
8 | const pheight = 600 |
9 | |
10 | const chunk_height = 2 // the image is recalculated in chunks, each chunk processed in a separate thread |
11 | |
12 | const zoom_factor = 1.1 |
13 | |
14 | const max_iterations = 255 |
15 | |
16 | struct ViewRect { |
17 | mut: |
18 | x_min f64 |
19 | x_max f64 |
20 | y_min f64 |
21 | y_max f64 |
22 | } |
23 | |
24 | fn (v &ViewRect) width() f64 { |
25 | return v.x_max - v.x_min |
26 | } |
27 | |
28 | fn (v &ViewRect) height() f64 { |
29 | return v.y_max - v.y_min |
30 | } |
31 | |
32 | struct AppState { |
33 | mut: |
34 | gg &gg.Context = unsafe { nil } |
35 | iidx int |
36 | pixels &u32 = unsafe { vcalloc(pwidth * pheight * sizeof(u32)) } |
37 | npixels &u32 = unsafe { vcalloc(pwidth * pheight * sizeof(u32)) } // all drawing happens here, results are swapped at the end |
38 | view ViewRect = ViewRect{-3.0773593290970673, 1.4952456603855397, -2.019938598189011, 2.3106642054225945} |
39 | scale int = 1 |
40 | ntasks int = runtime.nr_jobs() |
41 | } |
42 | |
43 | const colors = [gx.black, gx.blue, gx.red, gx.green, gx.yellow, gx.orange, gx.purple, gx.white, |
44 | gx.indigo, gx.violet, gx.black, gx.blue, gx.orange, gx.yellow, gx.green].map(u32(it.abgr8())) |
45 | |
46 | struct MandelChunk { |
47 | cview ViewRect |
48 | ymin f64 |
49 | ymax f64 |
50 | } |
51 | |
52 | fn (mut state AppState) update() { |
53 | mut chunk_channel := chan MandelChunk{cap: state.ntasks} |
54 | mut chunk_ready_channel := chan bool{cap: 1000} |
55 | mut threads := []thread{cap: state.ntasks} |
56 | defer { |
57 | chunk_channel.close() |
58 | threads.wait() |
59 | } |
60 | for t in 0 .. state.ntasks { |
61 | threads << spawn state.worker(t, chunk_channel, chunk_ready_channel) |
62 | } |
63 | // |
64 | mut oview := ViewRect{} |
65 | mut sw := time.new_stopwatch() |
66 | for { |
67 | sw.restart() |
68 | cview := state.view |
69 | if oview == cview { |
70 | time.sleep(5 * time.millisecond) |
71 | continue |
72 | } |
73 | // schedule chunks, describing the work: |
74 | mut nchunks := 0 |
75 | for start := 0; start < pheight; start += chunk_height { |
76 | chunk_channel <- MandelChunk{ |
77 | cview: cview |
78 | ymin: start |
79 | ymax: start + chunk_height |
80 | } |
81 | nchunks++ |
82 | } |
83 | // wait for all chunks to be processed: |
84 | for _ in 0 .. nchunks { |
85 | _ := <-chunk_ready_channel |
86 | } |
87 | // everything is done, swap the buffer pointers |
88 | state.pixels, state.npixels = state.npixels, state.pixels |
89 | println('${state.ntasks:2} threads; ${sw.elapsed().milliseconds():3} ms / frame; scale: ${state.scale:4}') |
90 | oview = cview |
91 | } |
92 | } |
93 | |
94 | [direct_array_access] |
95 | fn (mut state AppState) worker(id int, input chan MandelChunk, ready chan bool) { |
96 | for { |
97 | chunk := <-input or { break } |
98 | yscale := chunk.cview.height() / pheight |
99 | xscale := chunk.cview.width() / pwidth |
100 | mut x, mut y, mut iter := 0.0, 0.0, 0 |
101 | mut y0 := chunk.ymin * yscale + chunk.cview.y_min |
102 | mut x0 := chunk.cview.x_min |
103 | for y_pixel := chunk.ymin; y_pixel < chunk.ymax && y_pixel < pheight; y_pixel++ { |
104 | yrow := unsafe { &state.npixels[int(y_pixel * pwidth)] } |
105 | y0 += yscale |
106 | x0 = chunk.cview.x_min |
107 | for x_pixel := 0; x_pixel < pwidth; x_pixel++ { |
108 | x0 += xscale |
109 | x, y = x0, y0 |
110 | for iter = 0; iter < max_iterations; iter++ { |
111 | x, y = x * x - y * y + x0, 2 * x * y + y0 |
112 | if x * x + y * y > 4 { |
113 | break |
114 | } |
115 | } |
116 | unsafe { |
117 | yrow[x_pixel] = colors[iter & 15] |
118 | } |
119 | } |
120 | } |
121 | ready <- true |
122 | } |
123 | } |
124 | |
125 | fn (mut state AppState) draw() { |
126 | mut istream_image := state.gg.get_cached_image_by_idx(state.iidx) |
127 | istream_image.update_pixel_data(unsafe { &u8(state.pixels) }) |
128 | size := gg.window_size() |
129 | state.gg.draw_image(0, 0, size.width, size.height, istream_image) |
130 | } |
131 | |
132 | fn (mut state AppState) zoom(zoom_factor f64) { |
133 | c_x, c_y := (state.view.x_max + state.view.x_min) / 2, (state.view.y_max + state.view.y_min) / 2 |
134 | d_x, d_y := c_x - state.view.x_min, c_y - state.view.y_min |
135 | state.view.x_min = c_x - zoom_factor * d_x |
136 | state.view.x_max = c_x + zoom_factor * d_x |
137 | state.view.y_min = c_y - zoom_factor * d_y |
138 | state.view.y_max = c_y + zoom_factor * d_y |
139 | state.scale += if zoom_factor < 1 { 1 } else { -1 } |
140 | } |
141 | |
142 | fn (mut state AppState) center(s_x f64, s_y f64) { |
143 | c_x, c_y := (state.view.x_max + state.view.x_min) / 2, (state.view.y_max + state.view.y_min) / 2 |
144 | d_x, d_y := c_x - state.view.x_min, c_y - state.view.y_min |
145 | state.view.x_min = s_x - d_x |
146 | state.view.x_max = s_x + d_x |
147 | state.view.y_min = s_y - d_y |
148 | state.view.y_max = s_y + d_y |
149 | } |
150 | |
151 | // gg callbacks: |
152 | |
153 | fn graphics_init(mut state AppState) { |
154 | state.iidx = state.gg.new_streaming_image(pwidth, pheight, 4, pixel_format: .rgba8) |
155 | } |
156 | |
157 | fn graphics_frame(mut state AppState) { |
158 | state.gg.begin() |
159 | state.draw() |
160 | state.gg.end() |
161 | } |
162 | |
163 | fn graphics_click(x f32, y f32, btn gg.MouseButton, mut state AppState) { |
164 | if btn == .right { |
165 | size := gg.window_size() |
166 | m_x := (x / size.width) * state.view.width() + state.view.x_min |
167 | m_y := (y / size.height) * state.view.height() + state.view.y_min |
168 | state.center(m_x, m_y) |
169 | } |
170 | } |
171 | |
172 | fn graphics_move(x f32, y f32, mut state AppState) { |
173 | if state.gg.mouse_buttons.has(.left) { |
174 | size := gg.window_size() |
175 | d_x := (f64(state.gg.mouse_dx) / size.width) * state.view.width() |
176 | d_y := (f64(state.gg.mouse_dy) / size.height) * state.view.height() |
177 | state.view.x_min -= d_x |
178 | state.view.x_max -= d_x |
179 | state.view.y_min -= d_y |
180 | state.view.y_max -= d_y |
181 | } |
182 | } |
183 | |
184 | fn graphics_scroll(e &gg.Event, mut state AppState) { |
185 | state.zoom(if e.scroll_y < 0 { zoom_factor } else { 1 / zoom_factor }) |
186 | } |
187 | |
188 | fn graphics_keydown(code gg.KeyCode, mod gg.Modifier, mut state AppState) { |
189 | s_x := state.view.width() / 5 |
190 | s_y := state.view.height() / 5 |
191 | // movement |
192 | mut d_x, mut d_y := 0.0, 0.0 |
193 | if code == .enter { |
194 | println('> ViewRect{${state.view.x_min}, ${state.view.x_max}, ${state.view.y_min}, ${state.view.y_max}}') |
195 | } |
196 | if state.gg.pressed_keys[int(gg.KeyCode.left)] { |
197 | d_x -= s_x |
198 | } |
199 | if state.gg.pressed_keys[int(gg.KeyCode.right)] { |
200 | d_x += s_x |
201 | } |
202 | if state.gg.pressed_keys[int(gg.KeyCode.up)] { |
203 | d_y -= s_y |
204 | } |
205 | if state.gg.pressed_keys[int(gg.KeyCode.down)] { |
206 | d_y += s_y |
207 | } |
208 | state.view.x_min += d_x |
209 | state.view.x_max += d_x |
210 | state.view.y_min += d_y |
211 | state.view.y_max += d_y |
212 | // zoom in/out |
213 | if state.gg.pressed_keys[int(gg.KeyCode.left_bracket)] |
214 | || state.gg.pressed_keys[int(gg.KeyCode.z)] { |
215 | state.zoom(1 / zoom_factor) |
216 | return |
217 | } |
218 | if state.gg.pressed_keys[int(gg.KeyCode.right_bracket)] |
219 | || state.gg.pressed_keys[int(gg.KeyCode.x)] { |
220 | state.zoom(zoom_factor) |
221 | return |
222 | } |
223 | } |
224 | |
225 | fn main() { |
226 | mut state := &AppState{} |
227 | state.gg = gg.new_context( |
228 | width: 800 |
229 | height: 600 |
230 | create_window: true |
231 | window_title: 'The Mandelbrot Set' |
232 | init_fn: graphics_init |
233 | frame_fn: graphics_frame |
234 | click_fn: graphics_click |
235 | move_fn: graphics_move |
236 | keydown_fn: graphics_keydown |
237 | scroll_fn: graphics_scroll |
238 | user_data: state |
239 | ) |
240 | spawn state.update() |
241 | state.gg.run() |
242 | } |