sdl-gl.cpp (16758B)
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::string name; 210 std::vector<point> vertices; 211 std::vector<unsigned char> wireframe; 212 GLuint mode; 213 }; 214 215 std::vector<shp> shapes; 216 unsigned current_shape = 0; 217 218 struct object 219 { 220 circle pos; 221 rgb col; 222 float spin; 223 float rotation; 224 shp& shape; 225 }; 226 std::vector<std::shared_ptr<object>> objs = {}; 227 228 constexpr auto deref = std::views::transform([](auto const& p) -> auto& { return *p; }); 229 230 void presentScene(void) 231 { 232 glClearColor(0, 0, 0, 1); 233 glClear(GL_COLOR_BUFFER_BIT); 234 for (auto& o : objs | deref) { 235 o.rotation += o.spin / 60; 236 rotor r = rotor::angle(o.rotation); 237 if (o.pos.colliding(mouse_pos)) 238 glColor(o.col + rgb{.25f, .25f, .25f,}); 239 else 240 glColor(o.col); 241 242 if (not wireframe) 243 { 244 glBegin(o.shape.mode); 245 for (auto p : o.shape.vertices) 246 { 247 p *= o.pos.radius; 248 auto pp = (p * r + pan + o.pos.pos) * scale; 249 glVertex2f(pp.x / aspect_ratio, pp.y); 250 } 251 } 252 else 253 { 254 glBegin(GL_LINE_LOOP); 255 for (auto idx : o.shape.wireframe) 256 { 257 auto p = o.shape.vertices[idx]; 258 p *= o.pos.radius; 259 auto pp = (p * r + pan + o.pos.pos) * scale; 260 glVertex2f(pp.x / aspect_ratio, pp.y); 261 } 262 } 263 glEnd(); 264 } 265 glFinish(); 266 } 267 268 struct panel 269 { 270 screen_coord pos, size; 271 rgb colour; 272 void draw() const; 273 bool colliding(screen_coord c) 274 { 275 auto br = pos + size; 276 return c.x >= pos.x && c.y >= pos.y && c.x < br.x && c.y < br.y; 277 } 278 }; 279 280 panel menu; 281 bool details; 282 std::vector<panel> panels; 283 284 void panel::draw() const 285 { 286 auto tl = window_to_gl(pos); 287 auto tr = window_to_gl({pos.x + size.x, pos.y}); 288 auto bl = window_to_gl({pos.x, pos.y + size.y}); 289 auto br = window_to_gl(pos + size); 290 291 glBegin(GL_TRIANGLE_STRIP); 292 glColor(colour); 293 glVertex(tl); 294 glVertex(tr); 295 glVertex(bl); 296 glVertex(br); 297 glEnd(); 298 point half{1 / (2.f * screen.x), 1 / (2.f * screen.y)}; 299 300 tl.y -= half.y; 301 tr.y -= half.y; 302 bl.y += half.y; 303 br.y += half.y; 304 tr.x -= half.x; 305 br.x -= half.x; 306 tl.x += half.x; 307 bl.x += half.x; 308 309 glBegin(GL_LINE_STRIP); 310 glColor(colour / 2.f); 311 glVertex(bl); 312 glVertex(br); 313 glVertex(tr); 314 glEnd(); 315 316 glBegin(GL_LINE_STRIP); 317 glColor(colour * 1.5f); 318 glVertex(bl); 319 glVertex(tl); 320 glVertex(tr); 321 glEnd(); 322 } 323 324 void key(SDL_KeyboardEvent& e) 325 { 326 static std::uniform_real_distribution<float> size_dis{.5f, 1.5f}; 327 if (e.repeat) 328 return; 329 static uint8_t ctrls; 330 switch (e.keysym.sym) 331 { 332 case SDLK_LCTRL: 333 case SDLK_RCTRL: 334 ctrls += e.state ? 1 : -1; 335 break; 336 337 case SDLK_c: 338 if (e.state && ctrls) 339 exit(0); 340 break; 341 342 case SDLK_a: 343 if (e.state) 344 objs.push_back(std::make_shared<object>(circle{mouse_pos, size_dis(gen)}, rgb::random(), norm_dis(gen), 0, shapes[current_shape])); 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 loop: 410 SDL_MouseButtonEvent const& e = co_await click_event; 411 point drag_pos = pan - window_to_local({e.x, e.y}); 412 if (menu.colliding(mouse_coord)) 413 { 414 if (e.button == SDL_BUTTON_LEFT) 415 details = !details; 416 continue; 417 } 418 419 for (auto const& [i, p] : std::views::enumerate(panels)) 420 if (p.colliding(mouse_coord)) 421 { 422 if (e.button == SDL_BUTTON_LEFT) 423 current_shape = i; 424 goto loop; 425 } 426 427 switch (e.button) 428 { 429 case SDL_BUTTON_RIGHT: 430 { 431 for (;;) 432 { 433 SDL_Event const& e2 = co_await motion_or_release_event; 434 if (e2.type == SDL_MOUSEMOTION) 435 pan = drag_pos + window_to_local({e2.motion.x, e2.motion.y}); 436 else if (auto& b = e2.button; b.button == SDL_BUTTON_RIGHT) 437 break; 438 } 439 break; 440 } 441 case SDL_BUTTON_LEFT: 442 { 443 auto it = std::ranges::find_if(objs, [](auto const& o){return o->pos.colliding(mouse_pos);}); 444 if (it == std::end(objs)) 445 break; 446 447 drag_pos = (*it)->pos.pos - window_to_local({e.x, e.y}); 448 std::weak_ptr<object> wp = *it; 449 450 for (;;) 451 { 452 SDL_Event const& e2 = co_await motion_or_release_event; 453 if (wp.expired()) 454 break; 455 else if (e2.type == SDL_MOUSEMOTION) 456 wp.lock()->pos.pos = drag_pos + window_to_local({e2.motion.x, e2.motion.y}); 457 else if (auto& b = e2.button; b.button == SDL_BUTTON_LEFT) 458 break; 459 } 460 break; 461 } 462 } 463 } 464 } 465 466 void wheel(SDL_MouseWheelEvent* e) 467 { 468 float factor = exp(0.1f * e->y); 469 pan += window_to_local({e->mouseX, e->mouseY}) * (1/factor-1); 470 scale *= factor; 471 } 472 473 void window_event(SDL_WindowEvent const* e) 474 { 475 switch (e->event) 476 { 477 case SDL_WINDOWEVENT_SIZE_CHANGED: 478 screen.x = e->data1; 479 screen.y = e->data2; 480 aspect_ratio = (float) screen.x / screen.y; 481 glViewport(0, 0, screen.x, screen.y); 482 return; 483 } 484 } 485 486 void doInput() 487 { 488 SDL_Event event; 489 while (SDL_PollEvent(&event)) 490 { 491 switch (event.type) 492 { 493 case SDL_QUIT: 494 exit(0); 495 case SDL_KEYDOWN: 496 case SDL_KEYUP: 497 key(event.key); 498 break; 499 case SDL_MOUSEMOTION: 500 mouse_motion(event.motion); 501 motion_or_release_event(event); 502 break; 503 case SDL_MOUSEBUTTONDOWN: 504 click_event.ready(event.button); 505 break; 506 case SDL_MOUSEBUTTONUP: 507 motion_or_release_event(event); 508 break; 509 case SDL_MOUSEWHEEL: 510 wheel(&event.wheel); 511 break; 512 513 case SDL_WINDOWEVENT: 514 window_event(&event.window); 515 break; 516 517 default: 518 break; 519 } 520 } 521 } 522 523 void init_shape() 524 { 525 { 526 constexpr int n = 10; 527 shapes.emplace_back(); 528 auto& shape = shapes.back(); 529 shape.name = "star"; 530 shape.mode = GL_TRIANGLE_FAN; 531 shape.vertices.emplace_back(0,0); 532 for (int i = 0; i <= n; ++i) 533 shape.vertices.push_back(point{0, (.65f + i % 2) / -1.65f} * rotor::angle((float)i / n)); 534 for (int i = 0; i < n; ++i) 535 shape.wireframe.push_back(i+1); 536 } 537 { 538 constexpr int n = 3; 539 shapes.emplace_back(); 540 auto& shape = shapes.back(); 541 shape.name = "triangle"; 542 shape.mode = GL_TRIANGLES; 543 for (int i = 0; i < n; ++i) 544 shape.vertices.push_back(point{0, 1} * rotor::angle((float)i / n)); 545 for (int i = 0; i < n; ++i) 546 shape.wireframe.push_back(i); 547 } 548 { 549 constexpr int n = 4; 550 shapes.emplace_back(); 551 auto& shape = shapes.back(); 552 shape.name = "square"; 553 shape.mode = GL_TRIANGLE_FAN; 554 for (int i = 0; i < n; ++i) 555 shape.vertices.push_back(point{0, 1} * rotor::angle((float)i / n)); 556 for (int i = 0; i < n; ++i) 557 shape.wireframe.push_back(i); 558 } 559 { 560 constexpr int n = 64; 561 shapes.emplace_back(); 562 auto& shape = shapes.back(); 563 shape.name = "crescent"; 564 shape.mode = GL_TRIANGLE_STRIP; 565 for (int i = 0; i <= n; ++i) 566 { 567 float turns = i * .5f / n; 568 point p{0, 1}; 569 p *= rotor::angle(turns); 570 p.x *= 1 - i % 2 * .45f; 571 shape.vertices.push_back(p); 572 } 573 for (int i = 0; i <= n / 2; ++i) 574 shape.wireframe.push_back(i * 2); 575 for (int i = n / 2; i > 0; --i) 576 shape.wireframe.push_back(i * 2 - 1); 577 } 578 } 579 580 void init_scene() 581 { 582 objs.push_back(std::make_shared<object>(circle{{-1.5f, -1}, 1.f}, rgb{.7,.2,.2}, .2, 0, shapes[0])); 583 objs.push_back(std::make_shared<object>(circle{{0, 1}, .5f}, rgb{.2,.7,.2}, 0, 0, shapes[0])); 584 objs.push_back(std::make_shared<object>(circle{{1.5f, -1}, 1.f}, rgb{.2,.2,.7}, -.2, 0, shapes[0])); 585 } 586 587 struct psf2 588 { 589 unsigned int magic, version, hdr_size, flags, length, g_size, height, width; 590 }; 591 struct font 592 { 593 psf2 hdr; 594 GLuint tex; 595 font(char const* filename) 596 { 597 int fds[2]; 598 if (pipe(fds)) 599 err(1, "pipe"); 600 pid_t child = fork(); 601 if (child < 0) 602 err(1, "fork"); 603 if (!child) 604 { 605 if (dup2(fds[1], 1) == -1) 606 err(1, "dup2"); 607 if (close(fds[0])) 608 err(1, "close"); 609 if (close(fds[1])) 610 err(1, "close"); 611 execlp("gunzip", "gunzip", "--stdout", filename, nullptr); 612 err(1, "execlp"); 613 } 614 close(fds[1]); 615 616 if (read(fds[0], &hdr, sizeof(hdr)) != sizeof(hdr)) 617 err(1, "read"); 618 if (hdr.magic != 0x864AB572) 619 errx(1, "Not a PSF2 file"); 620 621 std::vector<unsigned char> storage(hdr.length * hdr.g_size); 622 623 for (ssize_t read = 0; read < (ssize_t)storage.size(); ) 624 { 625 int n = ::read(fds[0], storage.data() + read, storage.size() - read); 626 if (n < 0) 627 err(1, "read"); 628 if (n == 0) 629 errx(1, "not enough data from font"); 630 read += n; 631 } 632 close(fds[0]); 633 assert(wait(nullptr) == child); 634 std::vector<unsigned char> pix(hdr.length * hdr.height * hdr.width); 635 636 for (size_t i = 0; i < pix.size(); ++i) 637 pix[i] = ((storage[i / 8] >> (7 - i % 8)) & 1) * 255; 638 639 glGenTextures(1, &tex); 640 glBindTexture(GL_TEXTURE_2D, tex); 641 glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, hdr.width, hdr.height * hdr.length, 0, GL_ALPHA, GL_UNSIGNED_BYTE, pix.data()); 642 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 643 } 644 screen_coord draw(unsigned char c, screen_coord pos) 645 { 646 screen_coord br_screen{pos.x + (int)hdr.width, pos.y + (int)hdr.height}; 647 float text = (float)c / hdr.length; 648 float texb = (float)(c + 1u) / hdr.length; 649 auto tl = window_to_gl(pos); 650 auto tr = window_to_gl({br_screen.x, pos.y}); 651 auto bl = window_to_gl({pos.x, br_screen.y}); 652 auto br = window_to_gl(br_screen); 653 654 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 655 glEnable(GL_BLEND); 656 glEnable(GL_TEXTURE_2D); 657 glBegin(GL_TRIANGLE_STRIP); 658 glTexCoord2f(0,text); 659 glVertex(tl); 660 glTexCoord2f(1,text); 661 glVertex(tr); 662 glTexCoord2f(0,texb); 663 glVertex(bl); 664 glTexCoord2f(1,texb); 665 glVertex(br); 666 glEnd(); 667 glDisable(GL_TEXTURE_2D); 668 glDisable(GL_BLEND); 669 670 return br_screen; 671 } 672 screen_coord draw(std::string_view str, screen_coord pos) 673 { 674 for (auto c : str) 675 pos.x = draw(c, pos).x; 676 return pos; 677 } 678 }; 679 680 std::unique_ptr<font> fnt; 681 682 void draw_UI() 683 { 684 std::vector<std::string> strings; 685 strings.push_back(std::format("scale: {:.2}", scale)); 686 strings.push_back(std::format(" pan: ({:.1}, {:.1})", pan.x, pan.y)); 687 strings.push_back(std::format("{} shapes", shapes.size())); 688 strings.push_back(std::format("{} objects", objs.size())); 689 int width = (int)(2 + fnt->hdr.width * std::ranges::max(strings | std::views::transform([](auto const& s){ return s.size(); }))); 690 int height = (int)(2 + fnt->hdr.height * strings.size()); 691 menu.size = {width, height}; 692 menu.colour = menu.colliding(mouse_coord) ? rgb{.6, .6, .6} : rgb{.4, .4, .4}; 693 menu.draw(); 694 glColor3f(0, 0, 0); 695 int y = 1; 696 for (auto const& s : strings) 697 { 698 fnt->draw(s, {1, y}); 699 y += fnt->hdr.height; 700 } 701 702 y += 5; 703 704 panels.clear(); 705 706 for (auto const& [i, s] : std::views::enumerate(shapes)) 707 { 708 auto& p = panels.emplace_back( 709 screen_coord{0, y}, 710 screen_coord{width, (int)fnt->hdr.height}, 711 rgb{.4, .4, .4} 712 ); 713 if (p.colliding(mouse_coord)) 714 p.colour = rgb{.3, .6, .3}; 715 p.draw(); 716 if (current_shape == i) 717 glColor3f(1, 1, 1); 718 else 719 glColor3f(0, 0, 0); 720 fnt->draw(s.name, {1, y}); 721 y += fnt->hdr.height; 722 } 723 if (not details) 724 return; 725 726 auto x = (int)width; 727 strings.clear(); 728 for (auto const& [i, o] : objs | deref | std::views::enumerate) 729 strings.push_back(std::format("{}: size {}", i, o.pos.radius)); 730 731 if (strings.empty()) 732 return; 733 734 width = (int)(2 + fnt->hdr.width * std::ranges::max(strings | std::views::transform([](auto const& s){ return s.size(); }))); 735 height = (int)(2 + fnt->hdr.height * strings.size()); 736 panel{{x, 0}, {width, height}, rgb{.4, .4, .4}}.draw(); 737 glColor3f(0, 0, 0); 738 y = 1; 739 for (auto const& s : strings) 740 { 741 fnt->draw(s, {x + 1, y}); 742 y += fnt->hdr.height; 743 } 744 } 745 746 int main() 747 { 748 init_shape(); 749 init_scene(); 750 mouse_task(); 751 initSDL(); 752 atexit(SDL_Quit); 753 754 fnt.reset(new font{"/usr/share/kbd/consolefonts/lat9-16.psf.gz"}); 755 756 for (;;) 757 { 758 doInput(); 759 presentScene(); 760 draw_UI(); 761 SDL_GL_SwapWindow(window); 762 } 763 }