Browse Source

examples: Added initial examples infrastructure.

Ryan C. Gordon 9 months ago
parent
commit
5339b4458d

+ 183 - 0
build-scripts/build-web-examples.pl

@@ -0,0 +1,183 @@
+#!/usr/bin/perl -w
+
+# Simple DirectMedia Layer
+# Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+#
+# This software is provided 'as-is', without any express or implied
+# warranty.  In no event will the authors be held liable for any damages
+# arising from the use of this software.
+#
+# Permission is granted to anyone to use this software for any purpose,
+# including commercial applications, and to alter it and redistribute it
+# freely, subject to the following restrictions:
+#
+# 1. The origin of this software must not be misrepresented; you must not
+#    claim that you wrote the original software. If you use this software
+#    in a product, an acknowledgment in the product documentation would be
+#    appreciated but is not required.
+# 2. Altered source versions must be plainly marked as such, and must not be
+#    misrepresented as being the original software.
+# 3. This notice may not be removed or altered from any source distribution.
+
+use warnings;
+use strict;
+use File::Basename;
+use Cwd qw(abs_path);
+use IPC::Open2;
+
+my $examples_dir = abs_path(dirname(__FILE__) . "/../examples");
+my $project = undef;
+my $emsdk_dir = undef;
+my $compile_dir = undef;
+my $output_dir = undef;
+
+sub usage {
+    die("USAGE: $0 <project_name> <emsdk_dir> <compiler_output_directory> <html_output_directory>\n\n");
+}
+
+sub do_system {
+    my $cmd = shift;
+    $cmd = "exec /usr/bin/bash -c \"$cmd\"";
+    print("$cmd\n");
+    return system($cmd);
+}
+
+sub do_mkdir {
+    my $d = shift;
+    if ( ! -d $d ) {
+        print("mkdir '$d'\n");
+        mkdir($d) or die("Couldn't mkdir('$d'): $!\n");
+    }
+}
+
+sub build_latest {
+    # Try to build just the latest without re-running cmake, since that is SLOW.
+    print("Building latest version of $project ...\n");
+    if (do_system("EMSDK_QUIET=1 source '$emsdk_dir/emsdk_env.sh' && cd '$compile_dir' && ninja") != 0) {
+        # Build failed? Try nuking the build dir and running CMake from scratch.
+        print("\n\nBuilding latest version of $project FROM SCRATCH ...\n");
+        if (do_system("EMSDK_QUIET=1 source '$emsdk_dir/emsdk_env.sh' && rm -rf '$compile_dir' && mkdir '$compile_dir' && cd '$compile_dir' && emcmake cmake -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel .. && ninja") != 0) {
+            die("Failed to build latest version of $project!\n");  # oh well.
+        }
+    }
+}
+
+sub handle_example_dir {
+    my $category = shift;
+    my $example = shift;
+
+    my @files = ();
+    my $files_str = '';
+    opendir(my $dh, "$examples_dir/$category/$example") or die("Couldn't opendir '$examples_dir/$category/$example': $!\n");
+    my $spc = '';
+    while (readdir($dh)) {
+        next if not /\.c\Z/;  # only care about .c files.
+        my $path = "$examples_dir/$category/$example/$_";
+        next if not -f $path;    # only care about files.
+        push @files, $path;
+        $files_str .= "$spc$path";
+        $spc = ' ';
+    }
+    closedir($dh);
+
+    my $dst = "$output_dir/$category/$example";
+
+    do_mkdir($dst);
+    print("Building $category/$example ...\n");
+
+    # !!! FIXME: hardcoded SDL3 references, need to fix this for satellite libraries and SDL2.
+    do_system("EMSDK_QUIET=1 source '$emsdk_dir/emsdk_env.sh' && emcc -s USE_SDL=0 -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s MAXIMUM_MEMORY=1gb -s ASSERTIONS=0 -o '$dst/index.js' '-I$examples_dir/../include' $files_str '$compile_dir/libSDL3.a'") == 0
+        or die("Failed to build $category/$example!\n");
+
+    my $highlight_cmd = "highlight '--outdir=$dst' --style-outfile=highlight.css --fragment --stdout --syntax=c '--plug-in=$examples_dir/highlight-plugin.lua'";
+    print("$highlight_cmd\n");
+    my $pid = open2(my $child_out, my $child_in, $highlight_cmd);
+
+    my $htmlified_source_code = '';
+    foreach (@files) {
+        my $path = $_;
+        open my $srccode, '<', $path or die("Couldn't open '$path': $!\n");
+        my $fname = "$path";
+        $fname =~ s/\A.*\///;
+        print $child_in "/* $fname ... */\n\n";
+        while (<$srccode>) {
+            print $child_in $_;
+        }
+        close($srccode);
+    }
+
+    close($child_in);
+
+    while (<$child_out>) {
+        $htmlified_source_code .= $_;
+    }
+    close($child_out);
+
+    waitpid($pid, 0);
+
+
+    my $html = '';
+    open my $htmltemplate, '<', "$examples_dir/template.html" or die("Couldn't open '$examples_dir/template.html': $!\n");
+    while (<$htmltemplate>) {
+        s/\@project_name\@/$project/g;
+        s/\@category_name\@/$category/g;
+        s/\@example_name\@/$example/g;
+        s/\@htmlified_source_code\@/$htmlified_source_code/g;
+        $html .= $_;
+    }
+    close($htmltemplate);
+
+    open my $htmloutput, '>', "$dst/index.html" or die("Couldn't open '$dst/index.html': $!\n");
+    print $htmloutput $html;
+    close($htmloutput);
+
+    
+}
+
+sub handle_category_dir {
+    my $category = shift;
+
+    do_mkdir("$output_dir/$category");
+
+    opendir(my $dh, "$examples_dir/$category") or die("Couldn't opendir '$examples_dir/$category': $!\n");
+
+    while (readdir($dh)) {
+        next if ($_ eq '.') || ($_ eq '..');  # obviously skip current and parent entries.
+        next if not -d "$examples_dir/$category/$_";   # only care about subdirectories.
+        handle_example_dir($category, $_);
+    }
+
+    closedir($dh);
+}
+
+
+# Mainline!
+
+foreach (@ARGV) {
+    $project = $_, next if not defined $project;
+    $emsdk_dir = $_, next if not defined $emsdk_dir;
+    $compile_dir = $_, next if not defined $compile_dir;
+    $output_dir = $_, next if not defined $output_dir;
+    usage();  # too many arguments.
+}
+
+usage() if not defined $output_dir;
+
+build_latest();
+
+do_mkdir($output_dir);
+
+print("Examples dir: $examples_dir\n");
+
+opendir(my $dh, $examples_dir) or die("Couldn't opendir '$examples_dir': $!\n");
+
+while (readdir($dh)) {
+    next if ($_ eq '.') || ($_ eq '..');  # obviously skip current and parent entries.
+    next if not -d "$examples_dir/$_";   # only care about subdirectories.
+    handle_category_dir($_);
+}
+
+closedir($dh);
+
+exit(0);  # success!
+

+ 67 - 0
examples/README.md

@@ -0,0 +1,67 @@
+# Examples
+
+## What is this?
+
+In here are a collection of standalone SDL application examples. Unless
+otherwise stated, they should work on all supported platforms out of the box.
+If they don't [please file a bug to let us know](https://github.com/libsdl-org/SDL/issues/new).
+
+
+## What is this SDL_AppIterate thing?
+
+SDL can optionally build apps as a collection of callbacks instead of the
+usual program structure that starts and ends in a function called `main`.
+The examples use this format for two reasons.
+
+First, it allows the examples to work when built as web applications without
+a pile of ugly `#ifdef`s, and all of these examples are published on the web
+at [examples.libsdl.org](https://examples.libsdl.org/), so you can easily see
+them in action.
+
+Second, it's example code! The callbacks let us cleanly break the program up
+into the four logical pieces most apps care about:
+
+- Program startup
+- Event handling
+- What the program actually does in a single frame
+- Program shutdown
+
+A detailed technical explanation of these callbacks is in
+docs/README-main-functions.md (or view that page on the web on
+[the wiki](https://wiki.libsdl.org/SDL3/README/main-functions#main-callbacks-in-sdl3).
+
+
+## I would like to build and run these examples myself.
+
+When you build SDL with CMake, you can add `-DSDL_BUILD_EXAMPLES=On` to the
+CMake command line. When you build SDL, these examples will be built with it.
+
+But most of these can just be built as a single .c file, as long as you point
+your compiler at SDL3's headers and link against SDL.
+
+
+## What is the license on the example code? Can I paste this into my project?
+
+All code in the examples directory is considered public domain! You can do
+anything you like with it, including copy/paste it into your closed-source
+project, sell it, and pretend you wrote it yourself. We do not require you to
+give us credit for this code (but we always appreciate if you do!).
+
+This is only true for the examples directory. The rest of SDL falls under the
+[zlib license](https://github.com/libsdl-org/SDL/blob/main/LICENSE.txt).
+
+
+## What is template.html and highlight-plugin.lua in this directory?
+
+This is what [examples.libsdl.org](https://examples.libsdl.org/) uses when
+generating the web versions of these example programs. You can ignore this,
+unless you are improving it, in which case we definitely would love to hear
+from you!
+
+
+## What is template.c in this directory?
+
+If writing new examples, this is the skeleton code we start from, to keep
+everything consistent. You can ignore it.
+
+

+ 77 - 0
examples/highlight-plugin.lua

@@ -0,0 +1,77 @@
+-- This code adapted from https://gitlab.com/saalen/highlight/-/wikis/Plug-Ins
+
+-- first add a description of what the plug-in does
+Description="Add wiki.libsdl.org reference links to HTML, LaTeX or RTF output"
+
+-- define the plugin categories (ie. supported output formats; languages)
+Categories = { "c", "c++" }
+
+-- the syntaxUpdate function contains code related to syntax recognition
+function syntaxUpdate(desc)
+
+  -- if the current file is not C/C++ file we exit
+  if desc~="C and C++" then
+     return
+  end
+
+  -- this function returns a qt-project reference link of the given token
+  function getURL(token)
+     -- generate the URL
+     url='https://wiki.libsdl.org/SDL3/'.. token
+
+     -- embed the URL in a hyperlink according to the output format
+     -- first HTML, then LaTeX and RTF
+     if (HL_OUTPUT== HL_FORMAT_HTML or HL_OUTPUT == HL_FORMAT_XHTML) then
+         return '<a class="hl" target="new" href="'
+                .. url .. '">'.. token .. '</a>'
+     elseif (HL_OUTPUT == HL_FORMAT_LATEX) then
+         return '\\href{'..url..'}{'..token..'}'
+     elseif (HL_OUTPUT == HL_FORMAT_RTF) then
+         return '{{\\field{\\*\\fldinst HYPERLINK "'
+                ..url..'" }{\\fldrslt\\ul\\ulc0 '..token..'}}}'
+     end
+   end
+
+  -- the Decorate function will be invoked for every recognized token
+  function Decorate(token, state)
+
+    -- we are only interested in keywords, preprocessor or default items
+    if (state ~= HL_STANDARD and state ~= HL_KEYWORD and
+        state ~=HL_PREPROC) then
+      return
+    end
+
+    -- SDL keywords start with SDL_
+    -- if this pattern applies to the token, we return the URL
+    -- if we return nothing, the token is outputted as is
+    if string.find(token, "SDL_")==1 then
+      return getURL(token)
+    end
+
+  end
+end
+
+-- the themeUpdate function contains code related to the theme
+function themeUpdate(desc)
+  -- the Injections table can be used to add style information to the theme
+
+  -- HTML: we add additional CSS style information to beautify hyperlinks,
+  -- they should have the same color as their surrounding tags
+  if (HL_OUTPUT == HL_FORMAT_HTML or HL_OUTPUT == HL_FORMAT_XHTML) then
+    Injections[#Injections+1]=
+      "a.hl, a.hl:visited {color:inherit;font-weight:inherit;text-decoration:none}"
+
+  -- LaTeX: hyperlinks require the hyperref package, so we add this here
+  -- the colorlinks and pdfborderstyle options remove ugly boxes in the output
+  elseif (HL_OUTPUT==HL_FORMAT_LATEX) then
+    Injections[#Injections+1]=
+      "\\usepackage[colorlinks=false, pdfborderstyle={/S/U/W 1}]{hyperref}"
+  end
+end
+
+-- let highlight load the chunks
+Plugins={
+  { Type="lang", Chunk=syntaxUpdate },
+  { Type="theme", Chunk=themeUpdate },
+}
+

+ 80 - 0
examples/renderer/clear/renderer-clear.c

@@ -0,0 +1,80 @@
+/*
+ * This example code creates an SDL window and renderer, and then clears the
+ * window to a different color every frame, so you'll effectively get a window
+ * that's smoothly fading between colors.
+ *
+ * This code is public domain. Feel free to use it for any purpose!
+ */
+
+#define SDL_MAIN_USE_CALLBACKS 1  /* use the callbacks instead of main() */
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+
+
+/* We will use this renderer to draw into this window every frame. */
+static SDL_Window *window = NULL;
+static SDL_Renderer *renderer = NULL;
+
+/* the current red color we're clearing to. */
+static Uint8 red = 0;
+
+/* When fading up, this is 1, when fading down, it's -1. */
+static int fade_direction = 1;
+
+
+/* This function runs once at startup. */
+int SDL_AppInit(void **appstate, int argc, char *argv[])
+{
+    if (SDL_CreateWindowAndRenderer("examples/renderer/clear", 640, 480, 0, &window, &renderer) == -1) {
+        SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't create window/renderer!", SDL_GetError(), NULL);
+        return SDL_APP_FAILURE;
+    }
+    SDL_SetRenderVSync(renderer, 1);  /* try to show frames at the monitor refresh rate. */
+    return SDL_APP_CONTINUE;  /* carry on with the program! */
+}
+
+/* This function runs when a new event (mouse input, keypresses, etc) occurs. */
+int SDL_AppEvent(void *appstate, const SDL_Event *event)
+{
+    if (event->type == SDL_EVENT_QUIT) {
+        return SDL_APP_SUCCESS;  /* end the program, reporting success to the OS. */
+    }
+    return SDL_APP_CONTINUE;  /* carry on with the program! */
+}
+
+/* This function runs once per frame, and is the heart of the program. */
+int SDL_AppIterate(void *appstate)
+{
+    /* since we're always fading red, we leave green and blue at zero.
+       alpha doesn't mean much here, so leave it at full (255, no transparency). */
+    SDL_SetRenderDrawColor(renderer, red, 0, 0, 255);
+
+    /* clear the window to the draw color. */
+    SDL_RenderClear(renderer);
+
+    /* put the newly-cleared rendering on the screen. */
+    SDL_RenderPresent(renderer);
+
+    /* update the color for the next frame we will draw. */
+    if (fade_direction > 0) {
+        if (red == 255) {
+            fade_direction = -1;
+        } else {
+            red++;
+        }
+    } else if (fade_direction < 0) {
+        if (red == 0) {
+            fade_direction = 1;
+        } else {
+            red--;
+        }
+    }
+    return SDL_APP_CONTINUE;  /* carry on with the program! */
+}
+
+/* This function runs once at shutdown. */
+void SDL_AppQuit(void *appstate)
+{
+    /* SDL will clean up the window/renderer for us. */
+}
+

+ 45 - 0
examples/template.c

@@ -0,0 +1,45 @@
+/*
+ * This example code $WHAT_IT_DOES.
+ *
+ * This code is public domain. Feel free to use it for any purpose!
+ */
+
+#define SDL_MAIN_USE_CALLBACKS 1  /* use the callbacks instead of main() */
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+
+/* We will use this renderer to draw into this window every frame. */
+static SDL_Window *window = NULL;
+static SDL_Renderer *renderer = NULL;
+
+
+/* This function runs once at startup. */
+int SDL_AppInit(void **appstate, int argc, char *argv[])
+{
+    if (SDL_CreateWindowAndRenderer("examples/renderer/clear", 640, 480, 0, &window, &renderer) == -1) {
+        SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't create window/renderer!", SDL_GetError(), NULL);
+        return SDL_APP_FAILURE;
+    }
+    return SDL_APP_CONTINUE;  /* carry on with the program! */
+}
+
+/* This function runs when a new event (mouse input, keypresses, etc) occurs. */
+int SDL_AppEvent(void *appstate, const SDL_Event *event)
+{
+    if (event->type == SDL_EVENT_QUIT) {
+        return SDL_APP_SUCCESS;  /* end the program, reporting success to the OS. */
+    }
+    return SDL_APP_CONTINUE;  /* carry on with the program! */
+}
+
+/* This function runs once per frame, and is the heart of the program. */
+int SDL_AppIterate(void *appstate)
+{
+}
+
+/* This function runs once at shutdown. */
+void SDL_AppQuit(void *appstate)
+{
+    /* SDL will clean up the window/renderer for us. */
+}
+

+ 117 - 0
examples/template.html

@@ -0,0 +1,117 @@
+<!doctype html>
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>@project_name@ Example: @category_name@/@example_name@</title>
+    <style>
+      body {
+        font-family: arial;
+        margin: 0;
+        padding: none;
+      }
+
+      .emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
+      div.emscripten { text-align: center; }      
+      div.emscripten_border { border: 1px solid black; }
+      /* the canvas *must not* have any border or padding, or mouse coords will be wrong */
+      canvas.emscripten { border: 0px none; background-color: black; }
+
+      @-webkit-keyframes rotation {
+        from {-webkit-transform: rotate(0deg);}
+        to {-webkit-transform: rotate(360deg);}
+      }
+      @-moz-keyframes rotation {
+        from {-moz-transform: rotate(0deg);}
+        to {-moz-transform: rotate(360deg);}
+      }
+      @-o-keyframes rotation {
+        from {-o-transform: rotate(0deg);}
+        to {-o-transform: rotate(360deg);}
+      }
+      @keyframes rotation {
+        from {transform: rotate(0deg);}
+        to {transform: rotate(360deg);}
+      }
+
+      #output {
+        width: 100%;
+        height: 200px;
+        margin: 0 auto;
+        margin-top: 10px;
+        border-left: 0px;
+        border-right: 0px;
+        padding-left: 0px;
+        padding-right: 0px;
+        display: none;
+        background-color: black;
+        color: white;
+        font-family: 'Lucida Console', Monaco, monospace;
+        outline: none;
+      }
+
+      .source_code {
+      }
+    </style>
+    <link rel="stylesheet" type="text/css" href="highlight.css">
+  </head>
+  <body>
+    <div class="emscripten_border">
+      <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
+    </div>
+    <textarea id="output" rows="8"></textarea>
+
+    <div class="source_code">@htmlified_source_code@</div>
+
+    <script type='text/javascript'>
+      var Module = {
+        preRun: [],
+        postRun: [],
+        print: (function() {
+          var element = document.getElementById('output');
+          if (element) element.value = ''; // clear browser cache
+          return function(text) {
+            if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
+            // These replacements are necessary if you render to raw HTML
+            //text = text.replace(/&/g, "&amp;");
+            //text = text.replace(/</g, "&lt;");
+            //text = text.replace(/>/g, "&gt;");
+            //text = text.replace('\n', '<br>', 'g');
+            console.log(text);
+            if (element) {
+              element.value += text + "\n";
+              element.scrollTop = element.scrollHeight; // focus on bottom
+            }
+          };
+        })(),
+        canvas: (() => {
+          var canvas = document.getElementById('canvas');
+
+          // As a default initial behavior, pop up an alert when webgl context is lost. To make your
+          // application robust, you may want to override this behavior before shipping!
+          // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
+          canvas.addEventListener("webglcontextlost", (e) => { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
+
+          return canvas;
+        })(),
+        setStatus: (text) => {},
+        totalDependencies: 0,
+        monitorRunDependencies: (left) => {
+          this.totalDependencies = Math.max(this.totalDependencies, left);
+          Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
+        }
+      };
+      Module.setStatus('Downloading...');
+      window.onerror = (event) => {
+        // TODO: do not warn on ok events like simulating an infinite loop or exitStatus
+        Module.setStatus('Exception thrown, see JavaScript console');
+        Module.setStatus = (text) => {
+          if (text) console.error('[post-exception status] ' + text);
+        };
+      };
+    </script>
+    <script async type="text/javascript" src="index.js"></script>
+  </body>
+</html>
+
+