sdl-gl.cpp (15984B)
1 #include <GL/gl.h> 2 #include <SDL2/SDL.h> 3 #include <SDL2/SDL_events.h> 4 #include <SDL2/SDL_keycode.h> 5 #include <SDL2/SDL_mouse.h> 6 #include <SDL2/SDL_video.h> 7 #include <err.h> 8 9 #include "coroutine_owner.hpp" 10 11 #include <algorithm> 12 #include <cassert> 13 #include <exception> 14 #include <format> 15 #include <memory> 16 #include <numbers> 17 #include <print> 18 #include <random> 19 #include <ranges> 20 #include <vector> 21 22 #include <unistd.h> 23 #include <sys/wait.h> 24 #include <err.h> 25 26 extern "C" char const* __asan_default_options() { return "detect_leaks=0"; } 27 28 struct coroutine_task 29 { 30 struct promise_type 31 { 32 coroutine_task get_return_object(void) noexcept { return {}; } 33 std::suspend_never initial_suspend(void) const noexcept { return {}; } 34 std::suspend_never final_suspend() const noexcept { return {}; } 35 void unhandled_exception() const noexcept { std::terminate(); } 36 void return_void(void) const noexcept {} 37 }; 38 }; 39 40 template <typename T> 41 struct awaitable_value 42 { 43 bool ready(T& e) 44 { 45 assert(not event); 46 if (not waiting) 47 return false; 48 49 event = &e; 50 waiting.release().resume(); 51 return true; 52 } 53 bool operator()(T& e) 54 { 55 return ready(e); 56 } 57 58 bool await_ready() const 59 { 60 return event; 61 } 62 63 void await_suspend(std::coroutine_handle<> h) 64 { 65 assert(not waiting); 66 waiting.reset(h); 67 }; 68 69 T& await_resume() 70 { 71 assert(event); 72 return *std::exchange(event, nullptr); 73 } 74 75 private: 76 T* event = nullptr; 77 coroutine_owner<> waiting = {}; 78 }; 79 80 template <typename T> 81 struct vec2 82 { 83 T x = 0, y = 0; 84 vec2 operator -() const { return {-x, -y}; }; 85 vec2 operator *(T rhs) const { return {x * rhs, y * rhs}; } 86 vec2 operator /(T rhs) const { return {x / rhs, y / rhs}; } 87 vec2 operator +(vec2 rhs) const { return {x + rhs.x, y + rhs.y}; } 88 vec2 operator -(vec2 rhs) const { return *this + -rhs; } 89 vec2& operator *=(T rhs) { return *this = *this * rhs; } 90 vec2& operator /=(T rhs) { return *this = *this / rhs; } 91 vec2& operator +=(vec2 rhs) { return *this = *this + rhs; } 92 vec2& operator -=(vec2 rhs) { return *this = *this - rhs; } 93 T magnitude_squared() const { return x*x + y*y; } 94 }; 95 96 using point = vec2<float>; 97 using screen_coord = vec2<int>; 98 99 screen_coord screen = {800, 450}; 100 float aspect_ratio = (float) screen.x / screen.y; 101 102 bool wireframe; 103 104 SDL_Window* window; 105 106 void initSDL(void) 107 { 108 int windowFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE; 109 110 if (SDL_Init(SDL_INIT_VIDEO) < 0) 111 errx(1, "Couldn't initialize SDL: %s\n", SDL_GetError()); 112 113 window = SDL_CreateWindow("SDL OpenGL example", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen.x, screen.y, windowFlags); 114 115 if (!window) 116 errx(1, "Failed to open %d x %d window: %s\n", screen.x, screen.y, SDL_GetError()); 117 118 SDL_GLContext context = SDL_GL_CreateContext(window); 119 if (not context) 120 errx(1, "Failed to create OpenGL context: %s", SDL_GetError()); 121 if (SDL_GL_MakeCurrent(window, context)) 122 errx(1, "Failed to make OpenGL context current: %s", SDL_GetError()); 123 } 124 125 std::mt19937 gen; 126 std::lognormal_distribution<float> dis(-1, .75); 127 std::normal_distribution<float> norm_dis{0.f, .5f}; 128 129 struct rgb 130 { 131 float r,g,b; 132 133 rgb operator + (rgb const& rhs) const 134 { 135 return {r + rhs.r, g + rhs.g, b + rhs.b}; 136 } 137 138 rgb& operator *=(float rhs) { r *= rhs; g *= rhs; b *= rhs; return *this; } 139 rgb& operator /=(float rhs) { r /= rhs; g /= rhs; b /= rhs; return *this; } 140 rgb operator *(float rhs) const { return rgb{*this} *= rhs; } 141 rgb operator /(float rhs) const { return rgb{*this} /= rhs; } 142 143 static rgb random() 144 { 145 return {dis(gen), dis(gen), dis(gen)}; 146 } 147 }; 148 149 void glColor(rgb const& c) 150 { 151 glColor3f(c.r, c.g, c.b); 152 } 153 154 155 void glVertex(point p) 156 { 157 glVertex2f(p.x, p.y); 158 } 159 160 point window_to_gl(screen_coord c) 161 { 162 float screen_x = (2.f * c.x / screen.x - 1.f); 163 float screen_y = -2.f * c.y / screen.y + 1.f; 164 return {screen_x, screen_y}; 165 } 166 167 float scale = .5f; 168 169 point window_to_local(screen_coord c) 170 { 171 auto p = window_to_gl(c); 172 p.x *= aspect_ratio; 173 return p * (1/scale); 174 } 175 176 struct rotor 177 { 178 float s, xy; 179 static rotor angle(float turns) { float radians = turns * 2 * std::numbers::pi; return {cos(radians), sin(radians)}; } 180 }; 181 point operator *(point lhs, rotor rhs) 182 { 183 return {lhs.x * rhs.s - lhs.y * rhs.xy, lhs.x * rhs.xy + lhs.y * rhs.s}; 184 } 185 186 point& operator *=(point& lhs, rotor rhs) 187 { 188 return lhs = lhs * rhs; 189 } 190 191 struct circle 192 { 193 point pos; 194 float radius; 195 196 bool colliding(point p) const 197 { 198 return (pos - p).magnitude_squared() <= radius * radius; 199 } 200 }; 201 202 point pan = {0, 0}; 203 204 screen_coord mouse_coord; 205 point mouse_pos; 206 207 struct shp 208 { 209 std::vector<point> vertices; 210 std::vector<unsigned char> wireframe; 211 GLuint mode; 212 }; 213 214 std::vector<shp> shapes; 215 216 struct object 217 { 218 circle pos; 219 rgb col; 220 float spin; 221 float rotation; 222 shp& shape; 223 }; 224 std::vector<std::shared_ptr<object>> objs = {}; 225 226 constexpr auto deref = std::views::transform([](auto const& p) -> auto& { return *p; }); 227 228 void presentScene(void) 229 { 230 glClearColor(0, 0, 0, 1); 231 glClear(GL_COLOR_BUFFER_BIT); 232 for (auto& o : objs | deref) { 233 o.rotation += o.spin / 60; 234 rotor r = rotor::angle(o.rotation); 235 if (o.pos.colliding(mouse_pos)) 236 glColor(o.col + rgb{.25f, .25f, .25f,}); 237 else 238 glColor(o.col); 239 240 if (not wireframe) 241 { 242 glBegin(o.shape.mode); 243 for (auto p : o.shape.vertices) 244 { 245 p *= o.pos.radius; 246 auto pp = (p * r + pan + o.pos.pos) * scale; 247 glVertex2f(pp.x / aspect_ratio, pp.y); 248 } 249 } 250 else 251 { 252 glBegin(GL_LINE_LOOP); 253 for (auto idx : o.shape.wireframe) 254 { 255 auto p = o.shape.vertices[idx]; 256 p *= o.pos.radius; 257 auto pp = (p * r + pan + o.pos.pos) * scale; 258 glVertex2f(pp.x / aspect_ratio, pp.y); 259 } 260 } 261 glEnd(); 262 } 263 glFinish(); 264 } 265 266 struct panel 267 { 268 screen_coord pos, size; 269 rgb colour; 270 void draw() const; 271 bool colliding(screen_coord c) 272 { 273 auto br = pos + size; 274 return c.x >= pos.x && c.y >= pos.y && c.x < br.x && c.y < br.y; 275 } 276 }; 277 278 panel menu; 279 bool details; 280 281 void panel::draw() const 282 { 283 auto tl = window_to_gl(pos); 284 auto tr = window_to_gl({pos.x + size.x, pos.y}); 285 auto bl = window_to_gl({pos.x, pos.y + size.y}); 286 auto br = window_to_gl(pos + size); 287 288 glBegin(GL_TRIANGLE_STRIP); 289 glColor(colour); 290 glVertex(tl); 291 glVertex(tr); 292 glVertex(bl); 293 glVertex(br); 294 glEnd(); 295 point half{1 / (2.f * screen.x), 1 / (2.f * screen.y)}; 296 297 tl.y -= half.y; 298 tr.y -= half.y; 299 bl.y += half.y; 300 br.y += half.y; 301 tr.x -= half.x; 302 br.x -= half.x; 303 tl.x += half.x; 304 bl.x += half.x; 305 306 glBegin(GL_LINE_STRIP); 307 glColor(colour / 2.f); 308 glVertex(bl); 309 glVertex(br); 310 glVertex(tr); 311 glEnd(); 312 313 glBegin(GL_LINE_STRIP); 314 glColor(colour * 1.5f); 315 glVertex(bl); 316 glVertex(tl); 317 glVertex(tr); 318 glEnd(); 319 } 320 321 void key(SDL_KeyboardEvent& e) 322 { 323 static std::uniform_real_distribution<float> size_dis{.5f, 1.5f}; 324 if (e.repeat) 325 return; 326 static uint8_t ctrls; 327 switch (e.keysym.sym) 328 { 329 case SDLK_LCTRL: 330 case SDLK_RCTRL: 331 ctrls += e.state ? 1 : -1; 332 break; 333 334 case SDLK_c: 335 if (e.state && ctrls) 336 exit(0); 337 break; 338 339 case SDLK_a: 340 if (e.state) 341 { 342 static int i = 0; 343 objs.push_back(std::make_shared<object>(circle{mouse_pos, size_dis(gen)}, rgb::random(), norm_dis(gen), 0, shapes[i++ % shapes.size()])); 344 } 345 break; 346 347 case SDLK_d: 348 if (e.state) 349 std::erase_if(objs, [](auto& o) { return o->pos.colliding(mouse_pos); }); 350 break; 351 352 case SDLK_r: 353 if (e.state) 354 for (auto& obj : objs | deref) 355 if (obj.pos.colliding(mouse_pos)) 356 { 357 std::swap(obj.col.r, obj.col.g); 358 std::swap(obj.col.g, obj.col.b); 359 } 360 break; 361 362 case SDLK_EQUALS: 363 if (e.state) 364 for (auto& obj : objs | deref) 365 if (obj.pos.colliding(mouse_pos)) 366 obj.pos.radius *= 1.2; 367 break; 368 369 case SDLK_MINUS: 370 if (e.state) 371 for (auto& obj : objs | deref) 372 if (obj.pos.colliding(mouse_pos)) 373 obj.pos.radius /= 1.2; 374 break; 375 376 case SDLK_s: 377 if (e.state) 378 { 379 for (auto& obj : objs | deref) 380 if (obj.pos.colliding(mouse_pos)) 381 obj.spin *= -1; 382 383 } 384 break; 385 386 case SDLK_w: 387 if (e.state) 388 wireframe = !wireframe; 389 break; 390 391 case SDLK_ESCAPE: 392 exit(0); 393 } 394 } 395 396 void mouse_motion(SDL_MouseMotionEvent& e) 397 { 398 mouse_coord = {e.x, e.y}; 399 mouse_pos = window_to_local(mouse_coord) - pan; 400 } 401 402 awaitable_value<SDL_MouseButtonEvent const> click_event; 403 awaitable_value<SDL_Event const> motion_or_release_event; 404 405 coroutine_task mouse_task() 406 { 407 for (;;) 408 { 409 SDL_MouseButtonEvent const& e = co_await click_event; 410 point drag_pos = pan - window_to_local({e.x, e.y}); 411 if (menu.colliding(mouse_coord)) 412 { 413 if (e.button == SDL_BUTTON_LEFT) 414 details = !details; 415 continue; 416 } 417 switch (e.button) 418 { 419 case SDL_BUTTON_RIGHT: 420 { 421 for (;;) 422 { 423 SDL_Event const& e2 = co_await motion_or_release_event; 424 if (e2.type == SDL_MOUSEMOTION) 425 pan = drag_pos + window_to_local({e2.motion.x, e2.motion.y}); 426 else if (auto& b = e2.button; b.button == SDL_BUTTON_RIGHT) 427 break; 428 } 429 break; 430 } 431 case SDL_BUTTON_LEFT: 432 { 433 auto it = std::ranges::find_if(objs, [](auto const& o){return o->pos.colliding(mouse_pos);}); 434 if (it == std::end(objs)) 435 break; 436 437 drag_pos = (*it)->pos.pos - window_to_local({e.x, e.y}); 438 std::weak_ptr<object> wp = *it; 439 440 for (;;) 441 { 442 SDL_Event const& e2 = co_await motion_or_release_event; 443 if (wp.expired()) 444 break; 445 else if (e2.type == SDL_MOUSEMOTION) 446 wp.lock()->pos.pos = drag_pos + window_to_local({e2.motion.x, e2.motion.y}); 447 else if (auto& b = e2.button; b.button == SDL_BUTTON_LEFT) 448 break; 449 } 450 break; 451 } 452 } 453 } 454 } 455 456 void wheel(SDL_MouseWheelEvent* e) 457 { 458 float factor = exp(0.1f * e->y); 459 pan += window_to_local({e->mouseX, e->mouseY}) * (1/factor-1); 460 scale *= factor; 461 } 462 463 void window_event(SDL_WindowEvent const* e) 464 { 465 switch (e->event) 466 { 467 case SDL_WINDOWEVENT_SIZE_CHANGED: 468 screen.x = e->data1; 469 screen.y = e->data2; 470 aspect_ratio = (float) screen.x / screen.y; 471 glViewport(0, 0, screen.x, screen.y); 472 return; 473 } 474 } 475 476 void doInput() 477 { 478 SDL_Event event; 479 while (SDL_PollEvent(&event)) 480 { 481 switch (event.type) 482 { 483 case SDL_QUIT: 484 exit(0); 485 case SDL_KEYDOWN: 486 case SDL_KEYUP: 487 key(event.key); 488 break; 489 case SDL_MOUSEMOTION: 490 mouse_motion(event.motion); 491 motion_or_release_event(event); 492 break; 493 case SDL_MOUSEBUTTONDOWN: 494 click_event.ready(event.button); 495 break; 496 case SDL_MOUSEBUTTONUP: 497 motion_or_release_event(event); 498 break; 499 case SDL_MOUSEWHEEL: 500 wheel(&event.wheel); 501 break; 502 503 case SDL_WINDOWEVENT: 504 window_event(&event.window); 505 break; 506 507 default: 508 break; 509 } 510 } 511 } 512 513 void init_shape() 514 { 515 { 516 constexpr int n = 10; 517 shapes.emplace_back(); 518 auto& shape = shapes.back(); 519 shape.mode = GL_TRIANGLE_FAN; 520 shape.vertices.emplace_back(0,0); 521 for (int i = 0; i <= n; ++i) 522 shape.vertices.push_back(point{0, (.65f + i % 2) / -1.65f} * rotor::angle((float)i / n)); 523 for (int i = 0; i < n; ++i) 524 shape.wireframe.push_back(i+1); 525 } 526 { 527 constexpr int n = 3; 528 shapes.emplace_back(); 529 auto& shape = shapes.back(); 530 shape.mode = GL_TRIANGLES; 531 for (int i = 0; i < n; ++i) 532 shape.vertices.push_back(point{0, 1} * rotor::angle((float)i / n)); 533 for (int i = 0; i < n; ++i) 534 shape.wireframe.push_back(i); 535 } 536 { 537 constexpr int n = 4; 538 shapes.emplace_back(); 539 auto& shape = shapes.back(); 540 shape.mode = GL_TRIANGLE_FAN; 541 for (int i = 0; i < n; ++i) 542 shape.vertices.push_back(point{0, 1} * rotor::angle((float)i / n)); 543 for (int i = 0; i < n; ++i) 544 shape.wireframe.push_back(i); 545 } 546 { 547 constexpr int n = 64; 548 shapes.emplace_back(); 549 auto& shape = shapes.back(); 550 shape.mode = GL_TRIANGLE_STRIP; 551 for (int i = 0; i <= n; ++i) 552 { 553 float turns = i * .5f / n; 554 point p{0, 1}; 555 p *= rotor::angle(turns); 556 p.x *= 1 - i % 2 * .45f; 557 shape.vertices.push_back(p); 558 } 559 for (int i = 0; i <= n / 2; ++i) 560 shape.wireframe.push_back(i * 2); 561 for (int i = n / 2; i > 0; --i) 562 shape.wireframe.push_back(i * 2 - 1); 563 } 564 } 565 566 void init_scene() 567 { 568 objs.push_back(std::make_shared<object>(circle{{-1.5f, -1}, 1.f}, rgb{.7,.2,.2}, .2, 0, shapes[0])); 569 objs.push_back(std::make_shared<object>(circle{{0, 1}, .5f}, rgb{.2,.7,.2}, 0, 0, shapes[0])); 570 objs.push_back(std::make_shared<object>(circle{{1.5f, -1}, 1.f}, rgb{.2,.2,.7}, -.2, 0, shapes[0])); 571 } 572 573 struct psf2 574 { 575 unsigned int magic, version, hdr_size, flags, length, g_size, height, width; 576 }; 577 struct font 578 { 579 psf2 hdr; 580 GLuint tex; 581 font(char const* filename) 582 { 583 int fds[2]; 584 if (pipe(fds)) 585 err(1, "pipe"); 586 pid_t child = fork(); 587 if (child < 0) 588 err(1, "fork"); 589 if (!child) 590 { 591 if (dup2(fds[1], 1) == -1) 592 err(1, "dup2"); 593 if (close(fds[0])) 594 err(1, "close"); 595 if (close(fds[1])) 596 err(1, "close"); 597 execlp("gunzip", "gunzip", "--stdout", filename, nullptr); 598 err(1, "execlp"); 599 } 600 close(fds[1]); 601 602 if (read(fds[0], &hdr, sizeof(hdr)) != sizeof(hdr)) 603 err(1, "read"); 604 if (hdr.magic != 0x864AB572) 605 errx(1, "Not a PSF2 file"); 606 607 std::vector<unsigned char> storage(hdr.length * hdr.g_size); 608 609 for (ssize_t read = 0; read < (ssize_t)storage.size(); ) 610 { 611 int n = ::read(fds[0], storage.data() + read, storage.size() - read); 612 if (n < 0) 613 err(1, "read"); 614 if (n == 0) 615 errx(1, "not enough data from font"); 616 read += n; 617 } 618 close(fds[0]); 619 assert(wait(nullptr) == child); 620 std::vector<unsigned char> pix(hdr.length * hdr.height * hdr.width); 621 622 for (size_t i = 0; i < pix.size(); ++i) 623 pix[i] = ((storage[i / 8] >> (7 - i % 8)) & 1) * 255; 624 625 glGenTextures(1, &tex); 626 glBindTexture(GL_TEXTURE_2D, tex); 627 glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, hdr.width, hdr.height * hdr.length, 0, GL_ALPHA, GL_UNSIGNED_BYTE, pix.data()); 628 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 629 } 630 screen_coord draw(unsigned char c, screen_coord pos) 631 { 632 screen_coord br_screen{pos.x + (int)hdr.width, pos.y + (int)hdr.height}; 633 float text = (float)c / hdr.length; 634 float texb = (float)(c + 1u) / hdr.length; 635 auto tl = window_to_gl(pos); 636 auto tr = window_to_gl({br_screen.x, pos.y}); 637 auto bl = window_to_gl({pos.x, br_screen.y}); 638 auto br = window_to_gl(br_screen); 639 640 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 641 glEnable(GL_BLEND); 642 glEnable(GL_TEXTURE_2D); 643 glBegin(GL_TRIANGLE_STRIP); 644 glTexCoord2f(0,text); 645 glVertex(tl); 646 glTexCoord2f(1,text); 647 glVertex(tr); 648 glTexCoord2f(0,texb); 649 glVertex(bl); 650 glTexCoord2f(1,texb); 651 glVertex(br); 652 glEnd(); 653 glDisable(GL_TEXTURE_2D); 654 glDisable(GL_BLEND); 655 656 return br_screen; 657 } 658 screen_coord draw(std::string_view str, screen_coord pos) 659 { 660 for (auto c : str) 661 pos.x = draw(c, pos).x; 662 return pos; 663 } 664 }; 665 666 font* fnt; 667 668 void draw_UI() 669 { 670 std::vector<std::string> strings; 671 strings.push_back(std::format("scale: {:.2}", scale)); 672 strings.push_back(std::format(" pan: ({:.1}, {:.1})", pan.x, pan.y)); 673 strings.push_back(std::format("{} shapes", shapes.size())); 674 strings.push_back(std::format("{} objects", objs.size())); 675 int width = (int)(2 + fnt->hdr.width * std::ranges::max(strings | std::views::transform([](auto const& s){ return s.size(); }))); 676 int height = (int)(2 + fnt->hdr.height * strings.size()); 677 menu.size = {width, height}; 678 menu.colour = menu.colliding(mouse_coord) ? rgb{.6, .6, .6} : rgb{.4, .4, .4}; 679 menu.draw(); 680 glColor3f(0, 0, 0); 681 int y = 1; 682 for (auto const& s : strings) 683 { 684 fnt->draw(s, {1, y}); 685 y += fnt->hdr.height; 686 } 687 688 if (not details) 689 return; 690 691 auto x = (int)width; 692 strings.clear(); 693 for (auto const& [i, o] : objs | deref | std::views::enumerate) 694 strings.push_back(std::format("{}: size {}", i, o.pos.radius)); 695 696 if (strings.empty()) 697 return; 698 699 width = (int)(2 + fnt->hdr.width * std::ranges::max(strings | std::views::transform([](auto const& s){ return s.size(); }))); 700 height = (int)(2 + fnt->hdr.height * strings.size()); 701 panel{{x, 0}, {width, height}, rgb{.4, .4, .4}}.draw(); 702 glColor3f(0, 0, 0); 703 y = 1; 704 for (auto const& s : strings) 705 { 706 fnt->draw(s, {x + 1, y}); 707 y += fnt->hdr.height; 708 } 709 } 710 711 int main() 712 { 713 init_shape(); 714 init_scene(); 715 mouse_task(); 716 initSDL(); 717 atexit(SDL_Quit); 718 719 fnt = new font{"/usr/share/kbd/consolefonts/lat9-16.psf.gz"}; 720 721 for (;;) 722 { 723 doInput(); 724 presentScene(); 725 draw_UI(); 726 SDL_GL_SwapWindow(window); 727 } 728 }