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