├── .clang-format ├── .gitignore ├── LICENSE ├── README.md ├── build └── .empty └── src ├── asm ├── hello_pixel.asm └── stub.asm ├── build.jfdi ├── game.c ├── game.h ├── jfdi ├── limits.h └── main.c /.clang-format: -------------------------------------------------------------------------------- 1 | # main 2 | IndentWidth: 4 3 | BreakBeforeBraces: Linux 4 | AlwaysBreakAfterDefinitionReturnType: true 5 | UseTab: Never 6 | AllowShortIfStatementsOnASingleLine: false 7 | ColumnLimit: 80 8 | IndentCaseLabels: false 9 | 10 | # for macros 11 | AlignEscapedNewlinesLeft: false 12 | IndentPPDirectives: AfterHash 13 | 14 | # i love alignment 15 | BinPackArguments: false 16 | AllowAllParametersOfDeclarationOnNextLine: true 17 | BinPackParameters: false 18 | AlignTrailingComments: true 19 | AlignConsecutiveDeclarations: true 20 | AlignAfterOpenBracket: Align 21 | 22 | # most flow control must be indented for easy eye scanning 23 | AllowShortBlocksOnASingleLine: false 24 | AllowShortCaseLabelsOnASingleLine: false 25 | AllowShortLoopsOnASingleLine: true 26 | 27 | # braces 28 | BraceWrapping: 29 | BeforeElse: false 30 | AfterControlStatement: false 31 | AfterFunction: false 32 | AfterStruct: false 33 | AfterUnion: false 34 | IndentBraces: false 35 | 36 | # other 37 | AlignOperands: true 38 | MaxEmptyLinesToKeep: 3 39 | KeepEmptyLinesAtTheStartOfBlocks: false 40 | PointerAlignment: Left 41 | DerivePointerAlignment: false 42 | SpaceAfterCStyleCast: false 43 | SpaceBeforeAssignmentOperators: true 44 | SpaceBeforeParens: ControlStatements 45 | SpaceInEmptyParentheses: false 46 | SpacesBeforeTrailingComments: 2 47 | SpacesInCStyleCastParentheses: false 48 | SpacesInParentheses: false 49 | SpacesInSquareBrackets: false 50 | IncludeBlocks: Preserve 51 | SortIncludes: false 52 | BreakBeforeBinaryOperators: None 53 | ReflowComments: true 54 | 55 | 56 | # penalties 57 | PenaltyExcessCharacter: 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | *.obj 5 | *.elf 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Libraries 12 | *.lib 13 | *.a 14 | *.la 15 | *.lo 16 | 17 | # Shared objects (inc. Windows DLLs) 18 | *.dll 19 | *.so 20 | *.so.* 21 | *.dylib 22 | 23 | # Executables 24 | *.exe 25 | *.out 26 | *.app 27 | *.i*86 28 | *.x86_64 29 | *.hex 30 | bin/* 31 | .#* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michael Labbe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASM Funbox # 2 | 3 | This is the ASM Funbox. Giving you input and a framebuffer 60 times a second, write game logic in x64 assembly. 4 | 5 | Explore writing 64-bit assembly by writing a game without all of the setup or shutdown calls, focusing on pure game logic. 6 | 7 | ## Major Update ## 8 | 9 | This is ASM Funbox 2.0, which removes SCons and makes other quality of life updates. For legacy projects, ASM Funbox 1.0 is still available at the tag `asmfunbox_1` but is no longer supported. 10 | 11 | ## Compiling ## 12 | 13 | ### Dependencies ### 14 | 15 | ASM Funbox runs on Linux. Any VM that can run a fixed function OpenGL 16 | pipeline should be sufficient, in lieu of dedicated hardware. 17 | 18 | - Python for scons 19 | - nasm for assembling 20 | - SDL2 and OpenGL headers 21 | 22 | `apt-get install nasm libsdl2-dev libglu1-mesa-dev` should do it. 23 | 24 | ### Known Issues ### 25 | 26 | None. Report bugs! 27 | 28 | 29 | ## Controls ## 30 | 31 | See `sample_input_buttons` in `game.c` 32 | 33 | ## Building ## 34 | 35 | After installing dependencies (see above): 36 | 37 | ./make.py 38 | 39 | This produces executables in the bin subdirectory. 40 | 41 | ## Writing Your Own Game ## 42 | 43 | Each file in `src/asm` generates an exe with the same name under the `bin` directory. So, create `src/asm/game.asm` to generate `bin/game`. 44 | 45 | The game must implement a function called `asm_tick`. See `stub.asm` for the simplest example. 46 | 47 | The prototype of `asm_tick` is, effectively: 48 | 49 | int asm_tick(u8 buttons, uint32 *pixels, uint32 elapsed_ms); 50 | 51 | - If asm_tick returns 1, the program quits. 52 | 53 | - `buttons` is a set of bitflags, akin to a NES controller. See `BTN` constants in `game.c`. 54 | 55 | - `pixels` is a 160x90 32-bit RGBA image buffer. Write your pixels once per frame. 56 | 57 | - `elapsed_ms` is the number of elapsed milliseconds since program start 58 | 59 | ## Author ## 60 | 61 | [Michael Labbe](https://michaellabbe.com) 62 | 63 | -------------------------------------------------------------------------------- /build/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabbe/asmfunbox/a08f105e7c96da9512f4b965f42890f26878d5f0/build/.empty -------------------------------------------------------------------------------- /src/asm/hello_pixel.asm: -------------------------------------------------------------------------------- 1 | ;; --------------------------------------- 2 | ;; asm_tick 3 | ;; 4 | ;; rdi: Uint8 buttons 5 | ;; rsi: ptr Uint8 pixels[w*h*4] 6 | ;; rdx: Uint32 elapsed_ms 7 | ;; return int 0 or 1 for quit 8 | ;; --------------------------------------- 9 | 10 | ; parameters order: 11 | ; r9 ; 6th param 12 | ; r8 ; 5th param 13 | ; r10 ; 4th param 14 | ; rdx ; 3rd param 15 | ; rsi ; 2nd param 16 | ; rdi ; 1st param 17 | 18 | global asm_tick ; main entrypoint 19 | section .text 20 | asm_tick: 21 | 22 | test rdi, rdi ; test buttons for non-zero 23 | jnz quit 24 | 25 | 26 | 27 | ;; swap RSI/RDI so that rdi is pixel dst 28 | xor rsi, rdi 29 | xor rdi, rsi 30 | xor rsi, rdi 31 | 32 | ;; offset by one pixel row 33 | add rdi, 160*4 34 | 35 | ;; set rsi to a hardcoded value 36 | mov rsi, red 37 | call set_pixel 38 | 39 | add rdi, 1 40 | mov rsi, green 41 | call set_pixel 42 | 43 | add rdi, 1 44 | mov rsi, blue 45 | call set_pixel 46 | 47 | mov rax, 0 ; don't quit 48 | ret ; end tick 49 | 50 | quit: 51 | mov rax, 1 ; quit 52 | ret ; end tick 53 | 54 | ;; assumes rdi is pixel dst offset 55 | ;; rsi contains 32 bits of pixel data 56 | set_pixel: 57 | mov ecx, 3 58 | rep movsb 59 | ret 60 | 61 | section .data 62 | red: db 0xff, 0x00, 0x00 63 | green: db 0x00, 0xff, 0x00 64 | blue: db 0x00, 0x00, 0xff 65 | 66 | width: db 160 67 | height: db 90 68 | -------------------------------------------------------------------------------- /src/asm/stub.asm: -------------------------------------------------------------------------------- 1 | ;; --------------------------------------- 2 | ;; asm_tick 3 | ;; 4 | ;; rdi: Uint8 buttons 5 | ;; rsi: ptr Uint8 pixels[w*h*4] 6 | ;; rdx: Uint32 elapsed_ms 7 | ;; return int 0 or 1 for quit 8 | ;; --------------------------------------- 9 | 10 | global asm_tick ; main entrypoint 11 | section .text 12 | asm_tick: 13 | 14 | test rdi, rdi ; test buttons for non-zero 15 | jnz quit 16 | 17 | mov rax, 0 ; don't quit 18 | ret ; end tick 19 | 20 | quit: 21 | mov rax, 1 ; quit 22 | ret ; end tick 23 | -------------------------------------------------------------------------------- /src/build.jfdi: -------------------------------------------------------------------------------- 1 | # _______________ _____ 2 | # |_ | ___| _ \_ _| 3 | # | | |_ | | | | | | 4 | # | | _| | | | | | | 5 | # /\__/ / | | |/ / _| |_ 6 | # \____/\_| |___/ \___/ 7 | # 8 | # NOTE: 9 | # if you do not have jfdi.py, run this script with python to get it. 10 | # or, git clone https://github.com/mlabbe/jfdi 11 | """ 12 | jfdi build script 13 | 14 | available functions: 15 | cp(src, dst) - copy a file or directory 16 | rm(str|iter) - remove file or directory 17 | arg(str) - convert a /flag into a -flag depending on compiler 18 | use('?') - add make-like variables (LD, CC, etc.). gcc, clang, msvc 19 | cmd(list|str) - run a command on a shell, fatal if error, stdout returns as str 20 | die(str) - fail build with a message, errorlevel 3 21 | env(str) - return environment variable or None 22 | exe(str) - return filename with exe extension based on TARGET_OS 23 | exp(str) - expand a $string, searching CLI --vars and then global scope 24 | ext(str) - return file extension (file.c = .c) 25 | raw(str) - return file without extension (file.c = file) 26 | log(str) - print to stdout 27 | mkd(str) - make all subdirs 28 | obj(str) - return filename with obj file ext (file.c = file.obj) 29 | pth(str) - swap path slashes -- \ on windows, / otherwise 30 | var(str) - get buildvar passed in as a string, ie: DEBUG="0" 31 | yes(str) - get buildvar passed in as a boolean, ie: DEBUG=False 32 | 33 | variables: 34 | HOST_OS - compiling machine OS (str) 35 | TARGET_OS - target machine OS (str) 36 | 37 | after use(), variables, where applicable: 38 | CC - c compiler 39 | CXX - c++ compiler 40 | LD - linker 41 | OBJ - obj extension (ex: 'obj') 42 | CCTYPE - compiler 43 | CFLAGS - list of c flags 44 | CXXFLAGS - list of c++ flags 45 | LDFLAGS - list of linker flags 46 | 47 | """ 48 | 49 | JFDI_VERSION = 1 50 | 51 | INTERMEDIATE_DIR = '../build' 52 | BIN_DIR = "../bin" 53 | 54 | C_FILES = ('main.c', 'game.c') 55 | 56 | import os.path 57 | 58 | # bin/foo for asm/foo.asm 59 | def exe_for_asm_filename(in_asm_path): 60 | return BIN_DIR + '/' + exe(os.path.basename(in_asm_path)) 61 | 62 | # called at the start of the build 63 | def start_build(): 64 | use('clang') 65 | 66 | mkd(INTERMEDIATE_DIR) 67 | mkd(BIN_DIR) 68 | mkd(INTERMEDIATE_DIR + '/asm') 69 | 70 | 71 | # just always build without optimizations for the time being 72 | CFLAGS.extend(['-O0', '-g', '-Wall', '-pedantic', '--std=gnu99']) 73 | LDFLAGS.extend(['-lSDL2', '-lGL', '-lGLU', '-lm']) 74 | 75 | # build the c-side obj files, which are unchanged for each asm game 76 | for in_path in C_FILES: 77 | obj_path = obj(in_path, INTERMEDIATE_DIR) 78 | cmd(exp("$CC $CFLAGS -c $in_path -o $obj_path")) 79 | 80 | 81 | # return a list of files 82 | def list_input_files(): 83 | return ['asm/*.asm'] 84 | 85 | 86 | # return command to build single file in_path or None to skip 87 | def build_this(in_path): 88 | # if ext(in_path) == '.c': 89 | # obj_path = obj(in_path, INTERMEDIATE_DIR) 90 | # return exp("$CC $CFLAGS -c $in_path -o $obj_path") 91 | 92 | # todo: support different assemblers 93 | 94 | # assemble each asm into a .o 95 | asm_obj_path = obj(in_path, INTERMEDIATE_DIR) 96 | cmd(exp("nasm -f elf64 -o $asm_obj_path $in_path")) 97 | 98 | # link it 99 | bin_path = exe_for_asm_filename(in_path) 100 | c_objs = obj(C_FILES, INTERMEDIATE_DIR) 101 | return exp("$LD $c_objs $asm_obj_path -o $bin_path $LDFLAGS") 102 | 103 | # called after every input file has been built 104 | def end_build(in_files): 105 | #objs = obj(in_files, INTERMEDIATE_DIR) 106 | #cmd(exp("$LD $objs asm/hello_pixel.o -o $bin_path $LDFLAGS")) 107 | pass 108 | 109 | # called when the user runs 'jfdi clean' 110 | def clean(in_files): 111 | rm(INTERMEDIATE_DIR + '/*.o') 112 | 113 | for in_asm_path in in_files: 114 | rm(exe_for_asm_filename(in_asm_path)) 115 | 116 | # called when the user runs 'jfdi run' 117 | def run(): 118 | pass 119 | 120 | # 121 | # main -- installs build system if build script is run directly 122 | # 123 | # generated code: do not edit this 124 | # 125 | if __name__ == '__main__': 126 | import sys 127 | import os.path 128 | import urllib.request 129 | 130 | print("You have run the build script directly.") 131 | print("Expected Usage: python jfdi.py -f %s" % 132 | sys.argv[0]) 133 | 134 | DST_FILENAME = 'jfdi.py' 135 | if os.path.exists(DST_FILENAME): 136 | sys.exit(0) 137 | print("Do you want to download the JFDI build script?") 138 | yesno = input('Y/n -->') 139 | if yesno == 'n': 140 | sys.exit(0) 141 | 142 | print("downloading jfdi.py") 143 | url = "https://raw.githubusercontent.com/mlabbe/jfdi/master/jfdi.py" 144 | urllib.request.urlretrieve(url, DST_FILENAME) 145 | 146 | print("%s downloaded." % DST_FILENAME) 147 | print("Usage: python %s -f %s" % 148 | (DST_FILENAME, sys.argv[0])) 149 | print("To permanently install jfdi, manually copy jfdi.py into your search path.") 150 | sys.exit(0) 151 | 152 | -------------------------------------------------------------------------------- /src/game.c: -------------------------------------------------------------------------------- 1 | /* 2 | asmfunbox Copyright (C) 2014-2023 Frogtoss Games, Inc. 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "game.h" 10 | #include "limits.h" 11 | 12 | 13 | typedef struct { 14 | uint32_t frame_delta; 15 | uint32_t elapsed_ms; 16 | } game_timer_t; 17 | 18 | // BTN_* bitflags 19 | typedef uint8_t buttons_t; 20 | 21 | #define BTN_LEFT (1) 22 | #define BTN_RIGHT (1 << 1) 23 | #define BTN_UP (1 << 2) 24 | #define BTN_DOWN (1 << 3) 25 | #define BTN_SELECT (1 << 4) 26 | #define BTN_START (1 << 5) 27 | #define BTN_A (1 << 6) 28 | #define BTN_B (1 << 7) 29 | 30 | 31 | // this is the only function that needs to be implemented in assembly 32 | extern int asm_tick(buttons_t buttons, uint32_t* pixels, uint32_t elapsed_ms); 33 | 34 | // set this to true to not call into asm at all -- for debugging, only 35 | #define IMPLEMENT_GAME_IN_C 0 36 | 37 | static void 38 | surface_to_display(SDL_Surface* surface, GLuint* texture_handle) 39 | { 40 | /* perf: use glTexSubImage2D */ 41 | if (*texture_handle) { 42 | glDeleteTextures(1, texture_handle); 43 | } 44 | 45 | glGenTextures(1, texture_handle); 46 | glBindTexture(GL_TEXTURE_2D, *texture_handle); 47 | 48 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 49 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 50 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 51 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 52 | 53 | SDL_LockSurface(surface); 54 | glTexImage2D(GL_TEXTURE_2D, 55 | 0, 56 | GL_RGBA, 57 | BUFFER_W, 58 | BUFFER_H, 59 | 0, 60 | GL_RGBA, 61 | GL_UNSIGNED_BYTE, 62 | surface->pixels); 63 | SDL_UnlockSurface(surface); 64 | 65 | GLfloat w = 1.0f; 66 | GLfloat h = 1.0f; 67 | glBegin(GL_QUADS); 68 | 69 | glTexCoord2f(0.0f, 0.0f); 70 | glVertex2f(0.0f, 0.0f); 71 | 72 | glTexCoord2f(0.0f, 1.0f); 73 | glVertex2f(0.0f, h); 74 | 75 | glTexCoord2f(1.0f, 1.0f); 76 | glVertex2f(w, h); 77 | 78 | glTexCoord2f(1.0f, 0.0f); 79 | glVertex2f(w, 0.0f); 80 | 81 | glEnd(); 82 | } 83 | 84 | static void 85 | update_timer(game_timer_t* t) 86 | { 87 | uint32_t new_elapsed_ms = SDL_GetTicks(); 88 | t->frame_delta = new_elapsed_ms - t->elapsed_ms; 89 | t->elapsed_ms = new_elapsed_ms; 90 | 91 | if (t->frame_delta >= FRAME_COMPLAIN_THRESHOLD) { 92 | printf("long frame: %i ms\n", t->elapsed_ms); 93 | } 94 | } 95 | 96 | static void 97 | sample_input_buttons(int* quit, buttons_t* buttons) 98 | { 99 | SDL_Event event; 100 | 101 | #define SCANCODE event.key.keysym.scancode 102 | 103 | while (SDL_PollEvent(&event)) { 104 | switch (event.type) { 105 | case SDL_KEYUP: 106 | if (SCANCODE == SDL_SCANCODE_ESCAPE) 107 | *quit = 1; 108 | 109 | if (SCANCODE == SDL_SCANCODE_LEFT) 110 | *buttons &= ~BTN_LEFT; 111 | 112 | if (SCANCODE == SDL_SCANCODE_RIGHT) 113 | *buttons &= ~BTN_RIGHT; 114 | 115 | if (SCANCODE == SDL_SCANCODE_UP) 116 | *buttons &= ~BTN_UP; 117 | 118 | if (SCANCODE == SDL_SCANCODE_DOWN) 119 | *buttons &= ~BTN_DOWN; 120 | 121 | if (SCANCODE == SDL_SCANCODE_1) 122 | *buttons &= ~BTN_START; 123 | 124 | if (SCANCODE == SDL_SCANCODE_2) 125 | *buttons &= ~BTN_SELECT; 126 | 127 | if (SCANCODE == SDL_SCANCODE_A) 128 | *buttons &= ~BTN_A; 129 | 130 | if (SCANCODE == SDL_SCANCODE_B) 131 | *buttons &= ~BTN_B; 132 | 133 | break; 134 | 135 | case SDL_KEYDOWN: 136 | if (SCANCODE == SDL_SCANCODE_LEFT) 137 | *buttons |= BTN_LEFT; 138 | 139 | if (SCANCODE == SDL_SCANCODE_RIGHT) 140 | *buttons |= BTN_RIGHT; 141 | 142 | if (SCANCODE == SDL_SCANCODE_UP) 143 | *buttons |= BTN_UP; 144 | 145 | if (SCANCODE == SDL_SCANCODE_DOWN) 146 | *buttons |= BTN_DOWN; 147 | 148 | if (SCANCODE == SDL_SCANCODE_1) 149 | *buttons |= BTN_START; 150 | 151 | if (SCANCODE == SDL_SCANCODE_2) 152 | *buttons |= BTN_SELECT; 153 | 154 | if (SCANCODE == SDL_SCANCODE_A) 155 | *buttons |= BTN_A; 156 | 157 | if (SCANCODE == SDL_SCANCODE_B) 158 | *buttons |= BTN_B; 159 | break; 160 | } 161 | } 162 | 163 | #undef SCANCODE 164 | } 165 | 166 | #if IMPLEMENT_GAME_IN_C 167 | static void 168 | c_tick(buttons_t buttons, uint32_t* px, uint32_t elapsed_ms, SDL_PixelFormat* pixel_format) 169 | { 170 | Uint16 x, y; 171 | 172 | /* just do some silly stuff for now to prove frame updating is in. */ 173 | for (y = 0; y < BUFFER_H; ++y) { 174 | for (x = 0; x < BUFFER_W; ++x) { 175 | float fr, fg, fb; 176 | 177 | fr = (float)x / BUFFER_W; 178 | fg = (float)y / BUFFER_H; 179 | fb = (float)(x + y) / sqrtf(BUFFER_W + BUFFER_H); 180 | 181 | Uint8 r = (Uint8)(fr * 255.0f) | elapsed_ms; 182 | Uint8 g = (Uint8)(fg * 255.0f) + ~elapsed_ms; 183 | Uint8 b = (Uint8)((Uint32)(fb * 255.0f) % elapsed_ms); 184 | 185 | *px = SDL_MapRGB(pixel_format, r, g, b); 186 | px++; 187 | } 188 | } 189 | } 190 | 191 | #endif 192 | 193 | static void 194 | game_tick(int* quit, buttons_t* buttons, SDL_Surface* surf, game_timer_t* timer) 195 | { 196 | #if IMPLEMENT_GAME_IN_C 197 | SDL_LockSurface(surf->pixels); 198 | c_tick(*buttons, (uint32_t*)surf->pixels, timer->elapsed_ms, surf->format); 199 | SDL_UnlockSurface(surf->pixels); 200 | #else 201 | SDL_LockSurface(surf->pixels); 202 | asm_tick(*buttons, (uint32_t*)surf->pixels, timer->elapsed_ms); 203 | SDL_UnlockSurface(surf->pixels); 204 | #endif 205 | } 206 | 207 | 208 | void 209 | game_loop(void) 210 | { 211 | game_timer_t timer = {0}; 212 | 213 | int quit = 0; 214 | uint8_t buttons = 0; 215 | 216 | while (!quit) { 217 | update_timer(&timer); 218 | sample_input_buttons(&quit, &buttons); 219 | 220 | game_tick(&quit, &buttons, g_state.back, &timer); 221 | 222 | surface_to_display(g_state.back, &g_state.back_handle); 223 | SDL_GL_SwapWindow(g_state.window); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/game.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | /* 3 | asmfunbox Copyright (C) 2014-2023 Frogtoss Games, Inc. 4 | */ 5 | 6 | 7 | typedef struct { 8 | SDL_Window* window; 9 | SDL_GLContext gl_context; 10 | SDL_Surface* back; 11 | GLuint back_handle; 12 | } global_state_t; 13 | 14 | extern global_state_t g_state; /* main.c */ 15 | 16 | void game_loop(void); 17 | -------------------------------------------------------------------------------- /src/jfdi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # JFDI is 4 | # Copyright (C) 2016-2021 Frogtoss Games, Inc. 5 | # 6 | # Author Michael Labbe 7 | # See LICENSE in this repo for license terms 8 | # 9 | # latest version, examples and documentation: 10 | # https://github.com/mlabbe/jfdi.git 11 | 12 | # todo: 13 | # - handle OSError could not rmdir because a dos prompt is in it 14 | 15 | _cfg = {'verbose': False, 16 | 'build_vars': {} } 17 | 18 | import sys 19 | if sys.version_info[0] < 3: 20 | sys.stderr.write('JFDI requires Python 3\n') 21 | sys.stderr.write('Alternatively: Official github repo offers standalone exes for windows') 22 | sys.stderr.write('https://github.com/mlabbe/jfdi') 23 | sys.exit(1) 24 | 25 | import os 26 | import sys 27 | import glob 28 | import time 29 | import shutil 30 | import os.path 31 | import argparse 32 | import platform 33 | import subprocess 34 | 35 | VERSION=(1,0,6) 36 | 37 | g_start_time = time.time() 38 | 39 | def _is_jfdi_compatible_with_build_script_version(): 40 | """is this version of jfdi.py compatible with the build format version? 41 | The value is stored in JFDI_VERSION in the generated template. 42 | 43 | None return value means it's compatible""" 44 | script_version = int(globals()['JFDI_VERSION']) 45 | 46 | if script_version == 1: 47 | return None 48 | 49 | elif script_version > 1: 50 | return " is too old for build script JFDI_VERSION %d" % (script_version) 51 | 52 | elif script_version < 1: 53 | return " is too new for build script JFDI_VERSION %s" % (script_version) 54 | 55 | 56 | class _ArgDispatch: 57 | """subcommand-based argument processing. ie: 58 | jfdi init [args, options] 59 | jfdi clean [args, options] 60 | jfdi build [args, options] 61 | 62 | Also supports implicit subcommand for build: 63 | jfdi [args, options] 64 | """ 65 | def dispatch(self): 66 | desc = "JFDI Simple Build System version %s" % (_pp_version()) 67 | p = argparse.ArgumentParser( 68 | description=desc, 69 | usage='''%(prog)s [options] [buildvar=value ...] 70 | 71 | The most commonly used jfdi subcommands are: 72 | 73 | {jfdi} init # create new build.jfdi from template 74 | {jfdi} build # build your project 75 | {jfdi} clean # clean your project 76 | {jfdi} run # run your built project 77 | 78 | More help topics: 79 | 80 | {jfdi} help buildvars 81 | '''.format(jfdi='%(prog)s'), 82 | 83 | epilog="%(prog)s --help for detailed help") 84 | 85 | p.add_argument('subcommand', help='Subcommand to run (omit to build)', 86 | nargs='?', 87 | default='build') 88 | 89 | subcommand = sys.argv[1:2] 90 | 91 | # build is implicit subcommand; detect buildvar as first arg 92 | # and provide workaround. 93 | if len(subcommand) > 0 and '=' in subcommand[0]: 94 | subcommand = ['build'] 95 | 96 | # as above, but for actual arguments 97 | if len(subcommand) > 0 and subcommand[0][0] == '-': 98 | if subcommand[0] != '-h' and subcommand[0] != '--help': 99 | subcommand = ['build'] 100 | 101 | top_args = p.parse_args(subcommand) 102 | 103 | # default to 'build' if no subcommand is specified 104 | if not hasattr(self, 'subcommand_'+top_args.subcommand): 105 | _fatal_error("unknown subcommand %s.\n" % top_args.subcommand + 106 | "Use --help for detailed help") 107 | 108 | # call method named after command. if it returns at all, return a tuple: 109 | # 110 | # [0]: subcommand (implicit build is made explicit) 111 | # [1]: args namespace for subcommand 112 | # [2]: build vars dictionary 113 | # 114 | # callers are responsible for 1&2 115 | sub_args, build_vars = getattr(self, 'subcommand_'+top_args.subcommand)() 116 | 117 | # adjust global config from sub args 118 | global _cfg 119 | _cfg['verbose'] = sub_args.verbose 120 | _cfg['build_vars'] = build_vars 121 | 122 | return (top_args.subcommand, sub_args, build_vars) 123 | 124 | @staticmethod 125 | def _subcommand_prog(sub_name): 126 | return "%s %s" % (sys.argv[0], sub_name) 127 | 128 | @staticmethod 129 | def _add_common_args(p): 130 | """add args common to all subcommands 131 | """ 132 | p.add_argument('-f', '--file', 133 | help='read FILE as build.jfdi', 134 | default='build.jfdi') 135 | 136 | p.add_argument('-v', '--verbose', 137 | help="increase log verbosity", 138 | action='store_true') 139 | 140 | p.add_argument('--target-os', 141 | help='specify TARGET_OS for cross compiling', 142 | default=platform.system()) 143 | 144 | return p 145 | 146 | 147 | @staticmethod 148 | def _parse_build_vars(unknown_args): 149 | """build vars are KEY=value variables that are passed on the command 150 | line. parse them out of unknown args from parse_known_args 151 | and return a dict. 152 | """ 153 | # unknown arg parse sets variables 154 | vars = {} 155 | for v in unknown_args: 156 | var = v.split('=', 1) 157 | 158 | # if no equals sign exists in the unknown argument, the argument 159 | # is not a buildvar and is truly unknown 160 | if len(var) == 1: 161 | _fatal_error('"%s": unknown argument\n'% v) 162 | 163 | if len(var[1]) == 0: 164 | _fatal_error('"%s": buildvar must have value\n' % v) 165 | 166 | # all vars are uppercase 167 | ukey = var[0].upper() 168 | 169 | if ukey in vars: 170 | _fatal_error('"%s": buildvar specified multiple times\n' % v) 171 | 172 | vars[ukey] = var[1] 173 | 174 | return vars 175 | 176 | 177 | def subcommand_init(self): 178 | p = argparse.ArgumentParser( 179 | description='init creates a new build.jfdi in the working directory', 180 | prog=self._subcommand_prog('init'), 181 | ) 182 | 183 | p = self._add_common_args(p) 184 | 185 | sub_args = p.parse_args(sys.argv[2:]) 186 | 187 | return sub_args, {} # no build vars 188 | 189 | 190 | def subcommand_clean(self): 191 | 192 | p = argparse.ArgumentParser( 193 | description='clean intermediate and build product files by calling clean() in build.jfdi', 194 | prog=self._subcommand_prog('clean') 195 | ) 196 | 197 | p = self._add_common_args(p) 198 | 199 | sub_args, unknown_args = p.parse_known_args(sys.argv[2:]) 200 | build_vars = self._parse_build_vars(unknown_args) 201 | 202 | return sub_args, build_vars 203 | 204 | 205 | def subcommand_build(self): 206 | 207 | p = argparse.ArgumentParser( 208 | description="build the program by executing build.jfdi", 209 | prog=self._subcommand_prog('build'), 210 | ) 211 | 212 | p = self._add_common_args(p) 213 | 214 | p.add_argument('-r', '--run', help='call run() after successful build', 215 | action='store_true') 216 | p.add_argument('--version', help='print version and exit', 217 | action='store_true') 218 | 219 | # work around implicit build subcommand 220 | first_arg = 2 221 | if len(sys.argv) > 1 and sys.argv[1] != 'build': 222 | first_arg = 1 223 | 224 | sub_args, unknown_args = p.parse_known_args(sys.argv[first_arg:]) 225 | build_vars = self._parse_build_vars(unknown_args) 226 | 227 | return sub_args, build_vars 228 | 229 | 230 | def subcommand_run(self): 231 | 232 | p = argparse.ArgumentParser( 233 | description="perform a canonical run of the program by calling run() in build.jfdi", 234 | prog=self._subcommand_prog('run'), 235 | ) 236 | 237 | p = self._add_common_args(p) 238 | 239 | sub_args, unknown_args = p.parse_known_args(sys.argv[2:]) 240 | build_vars = self._parse_build_vars(unknown_args) 241 | 242 | return sub_args, build_vars 243 | 244 | 245 | def subcommand_help(self): 246 | 247 | p = argparse.ArgumentParser( 248 | description="additional help topics", 249 | prog=self._subcommand_prog('help'), 250 | ) 251 | 252 | p.add_argument('topic', help='Help topic. Use --help to see topics') 253 | 254 | sub_args = p.parse_args(sys.argv[2:]) 255 | sub_args.verbose = False 256 | 257 | 258 | return sub_args, {} 259 | 260 | 261 | def _which(file): 262 | for path in os.environ["PATH"].split(os.pathsep): 263 | if os.path.exists(os.path.join(path, file)): 264 | return os.path.join(path, file) 265 | return None 266 | 267 | def _pp_version(): 268 | """pretty print version as a string""" 269 | return '.'.join(str(i) for i in VERSION) 270 | 271 | def _clean(context, target_os): 272 | globals()['TARGET_OS'] = target_os 273 | _message(1, "cleaning") 274 | input_files = context[0]['list_input_files']() 275 | input_files = _handle_input_files(input_files) 276 | 277 | context[0]['clean'](input_files) 278 | # returning from clean means the calling script did not die(), and so 279 | # it was a success. 280 | 281 | 282 | 283 | def _message(verbosity, in_msg): 284 | global _cfg 285 | 286 | if in_msg.__class__ == list: 287 | msg = ' '.join(in_msg) 288 | else: 289 | msg = in_msg 290 | 291 | if verbosity >= 1 and not _cfg['verbose']: 292 | return 293 | print("%s %s" % (_log_stamp(), msg)) 294 | 295 | def _warning(msg): 296 | sys.stderr.write("%s WARNING: %s" % (_log_stamp(), msg)) 297 | 298 | def _fatal_error(msg, error_code=1): 299 | sys.stderr.write(_log_stamp() + ' FATAL: ') 300 | sys.stderr.write(msg) 301 | sys.stderr.write("exiting with error code %d\n" % error_code) 302 | sys.exit(error_code) 303 | 304 | def _get_script(args_file): 305 | """compiled contents of script or error out""" 306 | DEFAULT_SCRIPT = 'build.jfdi' 307 | 308 | script_path = None 309 | if args_file != None: 310 | script_path = args_file 311 | elif os.path.exists(DEFAULT_SCRIPT): 312 | script_path = DEFAULT_SCRIPT 313 | 314 | script_path = None 315 | if os.path.exists(DEFAULT_SCRIPT): 316 | script_path = DEFAULT_SCRIPT 317 | 318 | if args_file != None: 319 | script_path = args_file 320 | 321 | if script_path == None or not os.path.exists(script_path): 322 | fatal_msg = "Build file not found\n" 323 | fatal_msg += "\nIf this is your first run, use %s init\n" \ 324 | % sys.argv[0] 325 | fatal_msg += "%s --help for detailed help.\n\n" \ 326 | % sys.argv[0] 327 | _fatal_error(fatal_msg) 328 | 329 | with open(script_path) as f: 330 | script = f.read() 331 | 332 | try: 333 | pycode = compile(script, script_path, mode='exec') 334 | except SyntaxError as ex: 335 | msg = "SyntaxError in (%s, line %d):\n\t%s\n" \ 336 | % (ex.filename, ex.lineno, ex.text) 337 | _fatal_error(msg) 338 | return pycode 339 | 340 | def _swap_slashes(dir): 341 | if platform.system() == 'Windows': 342 | return dir.replace('/', '\\') 343 | else: 344 | return dir.replace('\\', '/') 345 | 346 | 347 | def _add_api(g): 348 | g['ext'] = _api_ext 349 | g['log'] = _api_log 350 | g['mkd'] = _api_mkd 351 | g['cmd'] = _api_cmd 352 | g['die'] = _api_die 353 | g['cp'] = _api_cp 354 | g['rm'] = _api_rm 355 | g['env'] = _api_env 356 | g['use'] = _api_use 357 | g['arg'] = _api_arg 358 | g['obj'] = _api_obj 359 | g['var'] = _api_var 360 | g['yes'] = _api_yes 361 | g['exe'] = _api_exe 362 | g['exp'] = _api_exp 363 | g['pth'] = _api_pth 364 | g['raw'] = _api_raw 365 | return g 366 | 367 | def _run_script(pycode, target_os): 368 | globals()['TARGET_OS'] = target_os 369 | g = _add_api(globals()) 370 | 371 | push_name = globals()['__name__'] 372 | globals()['__name__'] = '__jfdi__' 373 | exec(pycode, g) 374 | globals()['__name__'] = push_name 375 | 376 | # 377 | # validate expected functions 378 | # 379 | 380 | # todo: fill this out 381 | missing_msg = "" 382 | if 'list_input_files' not in g: 383 | missing_msg += "list_input_files() must exist\n" 384 | 385 | if 'JFDI_VERSION' not in g: 386 | missing_msg += "JFDI_VERSION must exist in build script\n" 387 | 388 | if len(missing_msg) != 0: 389 | sys.stderr.write("errors were found during execution:\n") 390 | _fatal_error(missing_msg) 391 | 392 | # 393 | # validate version compatibility 394 | # 395 | error_result = _is_jfdi_compatible_with_build_script_version() 396 | if error_result != None: 397 | _fatal_error("JFDI %s%s\n" % (_pp_version(), error_result)) 398 | 399 | 400 | context = [globals()] 401 | return context 402 | 403 | def _handle_input_files(input_files): 404 | if input_files.__class__ == str: 405 | input_files = [input_files] 406 | 407 | out_paths = [] 408 | 409 | for entry in input_files: 410 | if '*' in entry: 411 | wildcard = glob.glob(entry) 412 | for path in wildcard: 413 | out_paths.append(path) 414 | else: 415 | out_paths.append(entry) 416 | 417 | return out_paths 418 | 419 | 420 | def _build(context, target_os): 421 | globals()['HOST_OS'] = platform.system() 422 | globals()['TARGET_OS'] = target_os 423 | 424 | input_files = context[0]['list_input_files']() 425 | input_files = _handle_input_files(input_files) 426 | 427 | context[0]['start_build']() 428 | 429 | cmd_list = [] 430 | for path in input_files: 431 | cmd = context[0]['build_this'](path) 432 | if cmd != None: 433 | cmd_list.append(cmd) 434 | 435 | _message(1, "building %d/%d file(s)" % 436 | (len(cmd_list), len(input_files))) 437 | 438 | for cmd in cmd_list: 439 | _run_cmd(cmd) 440 | 441 | context[0]['end_build'](input_files) 442 | 443 | 444 | def _canonical_run(context, target_os): 445 | # not an error to have this omitted in the build script; run() is optional 446 | if 'run' not in context[0]: 447 | return 448 | 449 | globals()['HOST_OS'] = platform.system() 450 | globals()['TARGET_OS'] = target_os 451 | 452 | _message(1, "performing a canonical run of the build product") 453 | globals()['TARGET_OS'] = target_os 454 | context[0]['run']() 455 | 456 | def _run_cmd(cmd): 457 | _message(0, cmd) 458 | exit_code = subprocess.call(cmd, shell=True) 459 | if exit_code != 0: 460 | _fatal_error("error '%d' running command \"%s\"\n" % 461 | (exit_code, cmd)) 462 | 463 | def _report_success(start_time): 464 | end_time = time.time() 465 | delta_time = end_time - start_time 466 | _message(0, "exiting with success in %.1f seconds." % delta_time) 467 | 468 | def _str_to_list(val): 469 | """If val is str, return list with single entry, else return as-is.""" 470 | l = [] 471 | if val.__class__ == str: 472 | l.append(val) 473 | return l 474 | else: 475 | return val 476 | 477 | def _list_single_to_str(val): 478 | """If val is len(list) 1, return first entry, else return as-is.""" 479 | if val.__class__ == list and len(val) == 1: 480 | return val[0] 481 | else: 482 | return val 483 | 484 | 485 | def _log_stamp(): 486 | # strategy is to format in milliseconds until the build time 487 | # gets long, then format in terms of seconds. 488 | 489 | justify_chars = 6 490 | cur_time = time.time() 491 | 492 | d_ms = int((cur_time - g_start_time) * 1000) 493 | 494 | if d_ms < 1000: 495 | return "[%s ms]" % str(d_ms).rjust(justify_chars-2) 496 | else: 497 | d_s = cur_time - g_start_time 498 | 499 | num = "%.1f" % d_s 500 | return "[%ss]" % num.rjust(justify_chars) 501 | 502 | def _display_help_topic(topic): 503 | if topic == 'buildvars': 504 | msg='''Build Variables are KEY=value pairs specified on the commandline. 505 | In a build script, var('KEY') returns the value specified. 506 | 507 | Build variables have the following properties: 508 | 509 | REGARDING CASE: 510 | - They are case insensitive. var('key') and var('KEY') are the same thing. 511 | 512 | REGARDING FALSE/UNDEF IN BUILD SCRIPTS: 513 | - var('KEY') returns string, yes('KEY') returns boolean 514 | - Boolean KEY=0 on the command line should be evaulated as if yes('KEY') 515 | 516 | REGARDING UNSPECIFIED BUILD VARS: 517 | - var('OMITTED') == '' 518 | - yes('OMITTED') == False 519 | 520 | Example buildvar usage: 521 | jfdi DEBUG=1 522 | 523 | Corresponding buildvar code: 524 | if yes('debug'): 525 | CFLAGS.append('-g') 526 | 527 | ''' 528 | 529 | else: 530 | _fatal_error("unknown help topic %s. " % topic + 531 | "Use --help to view all help topics\n") 532 | 533 | print(msg) 534 | sys.exit(0) 535 | 536 | 537 | def generate_tmpl(path): 538 | if os.path.exists(path): 539 | _fatal_error("%s already exists.\n" % path) 540 | 541 | f = open(path, "wt") 542 | f.write("""\ 543 | # _______________ _____ 544 | # |_ | ___| _ \_ _| 545 | # | | |_ | | | | | | 546 | # | | _| | | | | | | 547 | # /\__/ / | | |/ / _| |_ 548 | # \____/\_| |___/ \___/ 549 | # 550 | # NOTE: 551 | # if you do not have jfdi.py, run this script with python to get it. 552 | # or, git clone https://github.com/mlabbe/jfdi 553 | \""" 554 | jfdi build script 555 | 556 | available functions: 557 | cp(src, dst) - copy a file or directory 558 | rm(str|iter) - remove file or directory 559 | arg(str) - convert a /flag into a -flag depending on compiler 560 | use('?') - add make-like variables (LD, CC, etc.). gcc, clang, msvc 561 | cmd(list|str) - run a command on a shell, fatal if error, stdout returns as str 562 | die(str) - fail build with a message, errorlevel 3 563 | env(str) - return environment variable or None 564 | exe(str) - return filename with exe extension based on TARGET_OS 565 | exp(str) - expand a $string, searching CLI --vars and then global scope 566 | ext(str) - return file extension (file.c = .c) 567 | raw(str) - return file without extension (file.c = file) 568 | log(str) - print to stdout 569 | mkd(str) - make all subdirs 570 | obj(str) - return filename with obj file ext (file.c = file.obj) 571 | pth(str) - swap path slashes -- \ on windows, / otherwise 572 | var(str) - get buildvar passed in as a string, ie: DEBUG="0" 573 | yes(str) - get buildvar passed in as a boolean, ie: DEBUG=False 574 | 575 | variables: 576 | HOST_OS - compiling machine OS (str) 577 | TARGET_OS - target machine OS (str) 578 | 579 | after use(), variables, where applicable: 580 | CC - c compiler 581 | CXX - c++ compiler 582 | LD - linker 583 | OBJ - obj extension (ex: 'obj') 584 | CCTYPE - compiler 585 | CFLAGS - list of c flags 586 | CXXFLAGS - list of c++ flags 587 | LDFLAGS - list of linker flags 588 | 589 | \""" 590 | 591 | JFDI_VERSION = 1 592 | 593 | # called at the start of the build 594 | def start_build(): 595 | pass 596 | 597 | # return a list of files 598 | def list_input_files(): 599 | return [] 600 | 601 | 602 | # return command to build single file in_path or None to skip 603 | def build_this(in_path): 604 | return None 605 | 606 | # called after every input file has been built 607 | def end_build(in_files): 608 | pass 609 | 610 | # called when the user runs 'jfdi clean' 611 | def clean(in_files): 612 | pass 613 | 614 | # called when the user runs 'jfdi run' 615 | def run(): 616 | pass 617 | 618 | # 619 | # main -- installs build system if build script is run directly 620 | # 621 | # generated code: do not edit this 622 | # 623 | if __name__ == '__main__': 624 | import sys 625 | import os.path 626 | import urllib.request 627 | 628 | print("You have run the build script directly.") 629 | print("Expected Usage: python jfdi.py -f %s" % 630 | sys.argv[0]) 631 | 632 | DST_FILENAME = 'jfdi.py' 633 | if os.path.exists(DST_FILENAME): 634 | sys.exit(0) 635 | print("Do you want to download the JFDI build script?") 636 | yesno = input('Y/n -->') 637 | if yesno == 'n': 638 | sys.exit(0) 639 | 640 | print("downloading jfdi.py") 641 | url = "https://raw.githubusercontent.com/mlabbe/jfdi/master/jfdi.py" 642 | urllib.request.urlretrieve(url, DST_FILENAME) 643 | 644 | print("%s downloaded." % DST_FILENAME) 645 | print("Usage: python %s -f %s" % 646 | (DST_FILENAME, sys.argv[0])) 647 | print("To permanently install jfdi, manually copy jfdi.py into your search path.") 648 | sys.exit(0) 649 | 650 | """) 651 | f.close() 652 | 653 | # 654 | # api 655 | # 656 | def _api_ext(x): 657 | return os.path.splitext(x)[1] 658 | 659 | def _api_raw(x): 660 | return os.path.splitext(x)[0] 661 | 662 | def _api_log(msg): 663 | frame = sys._getframe(1) 664 | func_name = frame.f_code.co_name 665 | 666 | print("%s %s(): %s" % (_log_stamp(), func_name, msg)) 667 | 668 | def _api_mkd(dirs): 669 | dirs = _swap_slashes(dirs) 670 | _message(1, "making dirs %s" % dirs) 671 | os.makedirs(dirs, exist_ok=True) 672 | 673 | def _api_cmd(cmd): 674 | if cmd.__class__ == list: 675 | cmd_str = ' '.join(cmd) 676 | else: 677 | cmd_str = cmd 678 | 679 | _message(0, cmd_str) 680 | 681 | proc = subprocess.Popen(cmd_str, shell=True, 682 | stdout=subprocess.PIPE, 683 | stderr=subprocess.PIPE) 684 | out, err = proc.communicate() 685 | ret = proc.returncode 686 | 687 | if len(err) != 0: 688 | _warning(err.decode('utf-8')) 689 | 690 | if ret != 0: 691 | print(out.rstrip().decode('utf-8'), file=sys.stdout) 692 | print(err.rstrip().decode('utf-8'), file=sys.stderr) 693 | _fatal_error("\nerror code %d running \"%s\"\n" % (ret, cmd_str), 694 | error_code=ret) 695 | 696 | return out.rstrip().decode('utf-8') 697 | 698 | def _api_cp(src, dst): 699 | if os.path.isdir(src): 700 | _message(0, "recursively copy %s to %s" % (src, dst)) 701 | shutil.copytree(src, dst) 702 | else: 703 | _message(0, "cp %s to %s" % (src, dst)) 704 | shutil.copy2(src, dst) 705 | 706 | def _api_die(msg): 707 | sys.stderr.write("die: " + msg + "\n") 708 | sys.exit(3) 709 | 710 | def _api_rm(files): 711 | 712 | file_list = _str_to_list(files) 713 | 714 | for f in file_list: 715 | if not os.path.exists(f): 716 | _message(1, "rm nonexistent %s" % f) 717 | continue 718 | 719 | f = _swap_slashes(f) 720 | 721 | if os.path.isdir(f): 722 | _message(0, "rmdir %s" % f) 723 | shutil.rmtree(f, ignore_errors=False) 724 | else: 725 | _message(0, "rm %s" % f) 726 | os.remove(f) 727 | 728 | def _api_env(e): 729 | if e not in os.environ: 730 | return None 731 | return os.environ[e] 732 | 733 | def _api_use(id): 734 | v = {} 735 | if id[:4] == 'msvc': 736 | v['CC'] = 'cl.exe' 737 | v['CXX'] = v['CC'] 738 | v['OBJ'] = 'obj' 739 | v['LD'] = 'link.exe' 740 | v['CCTYPE'] = 'msvc' 741 | v['CFLAGS'] = [] 742 | v['CXXFLAGS'] = [] 743 | v['LDFLAGS'] = [] 744 | 745 | elif id == 'clang': 746 | v['CC'] = 'clang' 747 | v['CXX'] = 'clang++' 748 | v['OBJ'] = 'o' 749 | v['LD'] = 'clang' # /usr/bin/ld is too low-level 750 | v['CCTYPE'] = 'gcc' # clang is gcc-like 751 | v['CFLAGS'] = [] 752 | v['CXXFLAGS'] = [] 753 | v['LDFLAGS'] = [] 754 | 755 | elif id == 'gcc': 756 | v['CC'] = 'gcc' 757 | v['CXX'] = 'g++' 758 | v['OBJ'] = 'o' 759 | v['LD'] = 'gcc' # /usr/bin/ld is too low-level 760 | v['CCTYPE'] = 'gcc' 761 | v['CFLAGS'] = [] 762 | v['CXXFLAGS'] = [] 763 | v['LDFLAGS'] = [] 764 | 765 | 766 | else: 767 | msg = "use() unknown ID '%s'\n" % (id) 768 | msg += "acceptable Ids:\n" 769 | msg += "\tmsvc, clang, gcc\n" 770 | _fatal_error(msg) 771 | 772 | # override from environment 773 | for potential_env in v: 774 | if potential_env in os.environ: 775 | v[potential_env] = os.environ[potential_env] 776 | 777 | if _which(v['CC']) == None: 778 | _warning("use(): compiler '%s' not found in search path.\n" % v['CC']) 779 | 780 | if _which(v['LD']) == None: 781 | _warning("use(): linker '%s' not found in search path.\n" % v['LD']) 782 | 783 | g = globals() 784 | for var in v: 785 | g[var] = v[var] 786 | 787 | def _api_arg(flag): 788 | if 'CCTYPE' not in globals(): 789 | _fatal_error("must call use() before arg()") 790 | 791 | i = 0 792 | if flag[0] == '-' or flag[0] == '/': 793 | i = 1 794 | 795 | if globals()['CCTYPE'] == 'msvc': 796 | symbol = '/' 797 | else: 798 | symbol = '-' 799 | 800 | return symbol + flag[i:] 801 | 802 | 803 | def _api_obj(path, in_prefix_path=''): 804 | prefix_path = _swap_slashes(in_prefix_path) 805 | if 'CCTYPE' not in globals(): 806 | _fatal_error('you must call use() before calling obj()\n') 807 | 808 | in_paths = _str_to_list(path) 809 | out_paths = [] 810 | 811 | ext = '' 812 | if globals()['CCTYPE'] == 'msvc': 813 | ext = '.obj' 814 | elif globals()['CCTYPE'] == 'gcc': 815 | ext = '.o' 816 | 817 | for p in in_paths: 818 | split = os.path.splitext(p) 819 | 820 | filename = split[0] + ext 821 | path = os.path.join(prefix_path, filename) 822 | 823 | out_paths.append(path) 824 | 825 | return _list_single_to_str(out_paths) 826 | 827 | 828 | def _api_var(key): 829 | global _cfg 830 | 831 | ukey = key.upper() 832 | if ukey not in _cfg['build_vars']: 833 | return '' 834 | 835 | return _cfg['build_vars'][ukey] 836 | 837 | 838 | def _api_yes(key): 839 | global _cfg 840 | 841 | ukey = key.upper() 842 | if ukey not in _cfg['build_vars']: 843 | return False 844 | 845 | if _cfg['build_vars'][ukey] == '0': 846 | return False 847 | 848 | # it is not possible to have a build var with a len(0) value so it 849 | # is safe to return True now. 850 | return True 851 | 852 | 853 | def _api_exe(path, append_if_debug=None): 854 | split = os.path.splitext(path) 855 | 856 | exe = '' 857 | if globals()['TARGET_OS'] == 'Windows': 858 | exe = '.exe' 859 | 860 | base_str = str(split[0]) 861 | if append_if_debug != None and _api_yes('DEBUG'): 862 | base_str += append_if_debug 863 | 864 | return base_str + exe 865 | 866 | def _api_exp(in_str): 867 | _message(1, "expanding \"%s\"" % in_str) 868 | out = '' 869 | 870 | reading_var = False 871 | for i in range(0, len(in_str)): 872 | c = in_str[i] 873 | if c == ' ': 874 | reading_var = False 875 | 876 | if reading_var: 877 | continue 878 | 879 | if c == '$': 880 | reading_var = True 881 | var = in_str[i:].split(' ')[0] 882 | if len(var) == 1: 883 | out += c 884 | reading_var = False 885 | continue 886 | 887 | var = var[1:] 888 | if len(var) > 1: 889 | val = None 890 | 891 | # scan calling function first 892 | frame = sys._getframe(1) 893 | if var in frame.f_locals: 894 | val = frame.f_locals[var] 895 | 896 | # scan vars second (command line override) 897 | elif var in _cfg['build_vars']: 898 | 899 | val = _cfg['build_vars'][var] 900 | # check environment variables, third 901 | elif var in os.environ: 902 | val = os.environ[var] 903 | 904 | # fall back to global vars 905 | elif var in globals(): 906 | val = globals()[var] 907 | else: 908 | _fatal_error("exp(): var %s not found.\n" % var) 909 | 910 | if val.__class__ == list: 911 | val = ' '.join(str(x) for x in val) 912 | 913 | out += val 914 | else: 915 | out += c 916 | 917 | return out 918 | 919 | def _api_pth(path): 920 | return _swap_slashes(path) 921 | 922 | 923 | # 924 | # main 925 | # 926 | 927 | if __name__ == '__main__': 928 | 929 | dispatch = _ArgDispatch() 930 | subcommand, args, build_vars = dispatch.dispatch() 931 | 932 | 933 | # 934 | # subcommand init 935 | # 936 | if subcommand == 'init': 937 | generate_tmpl(args.file) 938 | _message(1, 'wrote %s' % args.file) 939 | _report_success(g_start_time) 940 | sys.exit(0) 941 | 942 | # 943 | # subcommand help 944 | # 945 | if subcommand == 'help': 946 | _display_help_topic(args.topic) 947 | sys.exit(0) 948 | 949 | # 950 | # subcommand build, version case 951 | # 952 | if subcommand == 'build' and args.version: 953 | print(_pp_version()) 954 | sys.exit(0) 955 | 956 | # all subcommands not handled yet require execution of the build script. 957 | pycode = _get_script(args.file) 958 | context = _run_script(pycode, args.target_os) 959 | 960 | if subcommand == 'clean': 961 | # 962 | # subcommand clean 963 | # 964 | _clean(context, args.target_os) 965 | 966 | elif subcommand == 'run': 967 | # 968 | # subcommand run 969 | # 970 | _canonical_run(context, args.target_os) 971 | 972 | else: 973 | # 974 | # subcommand build (default) 975 | # 976 | _build(context, args.target_os) 977 | if args.run: 978 | _canonical_run(context, args.target_os) 979 | 980 | _report_success(g_start_time) 981 | sys.exit(0) 982 | 983 | -------------------------------------------------------------------------------- /src/limits.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | asmfunbox Copyright (C) 2014-2023 Frogtoss Games, Inc. 5 | */ 6 | 7 | 8 | #define WINDOW_W 1920 9 | #define WINDOW_H 1080 10 | 11 | // pixel buffer ratio to stretch over window 12 | #define BUFFER_W ((float)WINDOW_W / 12.f) 13 | #define BUFFER_H ((float)WINDOW_H / 12.f) 14 | 15 | 16 | #define FRAME_COMPLAIN_THRESHOLD 150 17 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | asmfunbox Copyright (C) 2014-2023 Frogtoss Games, Inc. 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "limits.h" 11 | #include "game.h" 12 | 13 | global_state_t g_state; 14 | 15 | 16 | static void 17 | fatal_sdl_error(char* str) 18 | { 19 | fprintf(stderr, "%s", str); 20 | fprintf(stderr, ": %s\n", SDL_GetError()); 21 | exit(1); 22 | } 23 | 24 | static void 25 | gl_sane_defaults(void) 26 | { 27 | /* glob all gl state initialization into one function because program is simple. */ 28 | glViewport(0, 0, (GLsizei)WINDOW_W, (GLsizei)WINDOW_H); 29 | 30 | glMatrixMode(GL_PROJECTION); 31 | glLoadIdentity(); 32 | gluOrtho2D(0.0, 1.0, 1.0, 0.0); 33 | 34 | glMatrixMode(GL_MODELVIEW); 35 | glLoadIdentity(); 36 | 37 | glClearColor(0.0, 0.0, 0.0, 0.0); 38 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 39 | glEnable(GL_TEXTURE_2D); 40 | 41 | // SDL_GL_SetSwapInterval(VSYNC); 42 | } 43 | 44 | 45 | int 46 | main(void) 47 | { 48 | // 49 | // startup 50 | if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_EVENTS) != 0) 51 | fatal_sdl_error("SDL_Init"); 52 | atexit(SDL_Quit); 53 | 54 | g_state.window = SDL_CreateWindow("ASM Funbox", 55 | SDL_WINDOWPOS_UNDEFINED, 56 | SDL_WINDOWPOS_UNDEFINED, 57 | WINDOW_W, 58 | WINDOW_H, 59 | SDL_WINDOW_OPENGL); 60 | 61 | if (!g_state.window) 62 | fatal_sdl_error("SDL_CreateWindow"); 63 | 64 | g_state.gl_context = SDL_GL_CreateContext(g_state.window); 65 | if (!g_state.gl_context) 66 | fatal_sdl_error("SDL_GL_CreateContext"); 67 | 68 | g_state.back = SDL_CreateRGBSurface(0, BUFFER_W, BUFFER_H, 32, 0, 0, 0, 0); 69 | if (!g_state.back) 70 | fatal_sdl_error("SDL_CreateRGBSurface"); 71 | 72 | gl_sane_defaults(); 73 | 74 | // 75 | // execute 76 | 77 | game_loop(); 78 | 79 | // 80 | // shutdown 81 | SDL_FreeSurface(g_state.back); 82 | SDL_DestroyWindow(g_state.window); 83 | 84 | 85 | return 0; 86 | } 87 | --------------------------------------------------------------------------------