The idea is to have a layered API structure, in which all layers are usable to engine’s users, and will be selected by convenience.
Enough said. The engine will not pretend it’s working with something else, and will allow/support direct OpenGL calls and access to OpenGL properties as much as possible.
The same idea applies to other dependencies that are directly related to drawing - e.g. image loading code.
This layer is already present and is not part of the engine code.
Lowest-level utilities wrapping together OpenGL operations in an interface that’s more convenient for most common operations.
Wrapping layers 0 and 1 in an object-based code that allows for convenient reuse of stateful data while carefully managing state underneath.
Not sure what’s the extent of resource management here. In particular, should the engine automagically manage resources like textures or image surfaces? Should caching be implemented underneath, so that e.g. image textue data is shared between instances of drawable quads?
Should we split those two into sublayers of a) resources, and b) drawable objects?
Or maybe provide layers for both automagically managed objects and not managed objects?
Resources: image, texture. Drawable: textured quad.
A high-level API for drawing things without having to manage any state.
Primarily done in terms of underlying API, will manage its resources to ensure maximum efficiency and minimum memory use.
- Where do e.g. particle systems fit in? As Layer-2 drawable objects?
How about we keep working on games and see what pops up?
I want an asset management system that’s flexible, but does not introduce any unnecessary cognitive overhead into the game code.
Critical assumption: an asset is always available as long as the handle exists (and is valid). Behind the scenes, the engine needs to be able to manage data so that the asset is ready when needed, including recreating it if it was somehow paged out.
2 goals:
- for development, we want errors to be loud and clear -> log + condition, debugger break
- for release, we want errors to be silent (non-crashing) -> log + some default values provided (e.g. default texture)
Continuable restarts in development mode for things that can be fixed - e.g. restart for loading a resource to provide a different path.
Something to use to toggle profiling, auto-saving perf counters, engine-level debug keys, etc.
NOTE make sure you check error values!
Either on load or on display. Point being, SDL_ttf seems to render text upside down and in weird format (BGRA maybe?). So this also needs proper handling of SDL_Surface image data.
Need an API proposal.
Simplest API proposal - direct drawing:
(draw-text "Hello world" :position position) ; uses default font and size
(draw-text "Hello world" :x x :y y) ; uses default font and size
(draw-text "Hello world" :x x :y y :size size) ; text with non-default size
(draw-text "Hello world" :position position :font some-font) ; uses specific font
(NOTE maybe draw-text* for :x :y, and draw-text for position?)
- load font
font types:
- ttf / open font types / etc.
- bitmap font (texture)
Maybe we can design the API so that rendered font’s size is the “best” size to use, but have the API rescale text automagically to fit in pixel size.
Also NOTE that while in testing, passed font’s height was 42 (pt, but SDL_ttf documentation says it’s essentially equivalent to px), the resulting texture was 47px high. Need to find out if this is some kind of border, or overline/underline magic, or what? Checking TTF_FontHeight would be cool, but that might require patching the bindings up.
Something to read configs from an external source. Also that would make games avoid writing over global engine variables so much.
Needs to work with both bundled games and ones run interactively from sources.
We’ll be implementing an entity/component/system architecture for managing the game.
(showing last few events that happened)
- :frame - sample every full frame
- :tick - sample every game tick (including every full frame if not in fixed-timestep mode) Or maybe just have the user use :frame when they know their game is not using a fixed timestep.
Actually, binding write-counter-report (+ name generation) to a keypress event should be enough.
Done; :after methods FTW.
Something like:Counter name | Interval | Increments | Sample |
MIN / RAVG / MAX | MIN / RAVG / MAX | ||
---|---|---|---|
Counter 1 | frame | 1.0 / 10.0 / 100.0 | 1.23 / 12.3 / 123.0 |
… | … | … | … |
Not that important, but useful for long sampling intervals and quick profiling sessions.
Not that important, but useful for long sampling intervals and quick profiling sessions.
Something to make using colors better / more convenient.
Something that would let me compose drawables (esp. text) and align them to each other instead of specifying positions manually.
Notes for internal profiler concept:
- You can probably collect samples in thread-local vars to avoid synchronization issues, and only merge them together when saving to JSON.
- Consider creating fixed-length buffers for samples and streaming them to files regularly, to avoid excessive memory usage. Note that 100x 1kb x 60 frames x 60 seconds = 360MB / minute.
- Also, fixed-length buffers would be great for avoiding generating lots of additional work for GC.
- Consider having start-profiling / stop-profiling functions that will let you start/stop recording, so you only generate samples you’re interested in.
- Try and not do all of these at once; a profiler like this seems to be a complicated task, and you’re supposed to be writing games, not performance debugging tools ;).
Scaffolding is done; what remains is integrating this into some sane resource management framework.
Something to free engine user from having to manually free resources (esp. drawables). Not sure if makes sense yet, maybe it’s better to stick to manual management for now.
Probably tracking will get integrated into it.
I’m interested in following primary performance counters:
- average render frame duration
- average updates per render frame
Represented as both number and a graph of change per second. Also I might be interested in graphing the sequence of update and render events in the last second.
Moreover, in the future, I may be interested in tracking down number of arbitrary events, so a generalized performance counter API could be in order.
NOTE that performance counters may count some of THE SAME data that will also be collected by the internal profiler (chrome://tracing), so it may be worth to consider to merge the two concepts together.
Instead of in-game graph, consider (maybe as a prototype) simply dumping perf counter data to a file to be graphed later.
It’ll take some serious work (esp. with getting it not to crash or fail on type checks); maybe it’s more worth it to switch to rtg-math.
In particular, in TSWR: Asteroids I’m using an ECS system for collision detection.
First, debug the memory profile of the engine / games running on top of it, and then implement it if necessary.
What I want my text to handle, at least in the future:
- multiline rendering to a box of given size (in pixels)
- alignment within a box of given size (in pixels)
- policies for said box (let the text spill out, or actually do overflow: hidden)
- colors
- bold, italics, underline, overline
- being able to access the text and replace some of its parts with a different one (for things like on-hover effects)
- all features need to be somewhat supported by both bitmap and rendered fonts transparently, so that - past loading a font - I don’t have to care about type of font anymore
Some DSL like:
(:align :right :effect :underline "Hello World")
(:align :center :color #x11FF11 :id "affaff" "Maka paka")
("Foo" (:on-hover (lambda (_this) "Quux") "Bar"))
("Foo" (:on-hover (lambda (this) (:color "red" (text this))) "Bar!"))
Also, aligning/positioning DSL would be cute.
See https://github.com/libgdx/libgdx/wiki/Distance-field-fonts for details.
Some nice web-based viewer UI for SBCL’s sb-sprof, using https://datatables.net/ to navigate the dump.
Useful for e.g. resizable UI elements.
http://anttweakbar.sourceforge.net/doc/
Or maybe integrating with precisely it!
Bonus feature: I’d like to be able to dump the changes to string / file, so that I could use it to experiment with stuff at runtime, and then easily list changes to introduce to code.
Either way, that would need to have some utilities for discovery. Maybe we should map things available for cmdlist / cvarlist with annotations in code? Actually, that could also make sense with the GUI AntTweakBar idea.
Something like:
@Tweakable (:type :color)
(defparameter +default-enemy-color+ (make-color "#11FF11") "Default enemy color.")
Leading to both it being available as cvar, and as a color gauge in the Tweak Bar, with the description taken from the docstring.
Stored in issues.
Debug performance counters keep giving me something like this (with step temporarily limited to 1/50s):<DEBUG> [00:03:47] p2d main.lisp (run-main-loop) - In last second, executed 50 steps over 16 milliseconds, AVG = 0.32 ms/step. <DEBUG> [00:03:47] p2d main.lisp (run-main-loop) - Rendered 60 frames over 983 milliseconds, AVG = 16.383333 ms/frame.
It’s like there’s vsync enabled somewhere by default. I want to control that setting.
NOTE: (sdl2:gl-get-swap-interval) returns 1 - vsync is on, apparently. NOTE2: (sdl2:gl-set-swap-interval) controls vsync.
Right now they’ll only work with a fixed step loop.<DEBUG> [00:22:59] p2d main.lisp (run-main-loop) - In last second, executed 60 steps over 6 milliseconds, AVG = 0.1 ms/step. <DEBUG> [00:22:59] p2d main.lisp (run-main-loop) - Rendered 2732 frames over 981 milliseconds, AVG = 0.3590776 ms/frame.
Compare vs. vsync enabled:
<DEBUG> [00:25:07] p2d main.lisp (run-main-loop) - In last second, executed 60 steps over 50 milliseconds, AVG = 0.8333333 ms/step. <DEBUG> [00:25:07] p2d main.lisp (run-main-loop) - Rendered 60 frames over 949 milliseconds, AVG = 15.816667 ms/frame.
Might be the artifact of the way I calculate step time though. I.e. bug in the debug counters.
Need to investigate why, and - if necessary - enable/disable vsync functionality based on some system information (e.g. hardcoded if vendor == Chromium). Or maybe trap the error instead and use this as an indicator that vsync functionality is not supported on the platform.
Getting texture loading right.
- supported pixelformats -> textures
- unsupported pixelformats should barf an error
- flipping if needed (not sure if it can be determined from surface data though)
Pixel data types we need to care about: GL_RGB, GL_BGR, GL_RGBA, GL_BGRA. Internal data types we need to care about: GL_RGB, GL_RGBA, and maybe GL_SRGB8 and GL_SRGB8_ALPHA_8
SDL_ConvertSurface (or rather, SDL_ConvertSurfaceFormat) could be useful to get it into a proper format if it isn’t in one. Actually, unless we hit performance limit, this would be preferred solution because of simplicity. (setf use-alpha (ispixelformat-alpha surface->format))
(if (or (ispixelformat-indexed surface->format) (and (not (= (bytes-per-pixel surface->format 3))) (not (= (bytes-per-pixel surface->format 4))) (convert-surface surface use-alpha) (return (if use-alpha :rgba :rgb)))
;; TODO handling of RGB / BGR and RGBA / BGRA has-blue-channel-first –> BGR24 | BGR888 | BGRA8888
NOTE: X86 is little-endian, FWIW.
Right now we went the lazy way and did a force-convert with SDL_ConvertSurfaceFormat. Would be cool to replace it in the future because of performance and memory reasons.
<DEBUG> [00:42:34] p2d filegNgwby (load-debug-images-and-dump-info load-img-and-dump-info) - PARENDECK2D::IMG: “assets/trc_tex.gif” (SDL2:SURFACE-WIDTH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-HEIGHT PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-PITCH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-FORMAT PARENDECK2D::LOADED-IMG): #.(SB-SYS:INT-SAP #X7FFFDC3B8A10) (SDL2:SURFACE-FORMAT-FORMAT PARENDECK2D::LOADED-IMG): :INDEX8
<DEBUG> [00:42:34] p2d filegNgwby (load-debug-images-and-dump-info load-img-and-dump-info) - PARENDECK2D::IMG: “assets/trc_tex.jpg” (SDL2:SURFACE-WIDTH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-HEIGHT PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-PITCH PARENDECK2D::LOADED-IMG): 384 (SDL2:SURFACE-FORMAT PARENDECK2D::LOADED-IMG): #.(SB-SYS:INT-SAP #X7FFFDC3C68E0) (SDL2:SURFACE-FORMAT-FORMAT PARENDECK2D::LOADED-IMG): :RGB24
<DEBUG> [00:42:34] p2d filegNgwby (load-debug-images-and-dump-info load-img-and-dump-info) - PARENDECK2D::IMG: “assets/trc_tex.tga” (SDL2:SURFACE-WIDTH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-HEIGHT PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-PITCH PARENDECK2D::LOADED-IMG): 512 (SDL2:SURFACE-FORMAT PARENDECK2D::LOADED-IMG): #.(SB-SYS:INT-SAP #X7FFFDC42D870) (SDL2:SURFACE-FORMAT-FORMAT PARENDECK2D::LOADED-IMG): :ARGB8888
<DEBUG> [00:42:34] p2d filegNgwby (load-debug-images-and-dump-info load-img-and-dump-info) - PARENDECK2D::IMG: “assets/trc_tex_24bit.bmp” (SDL2:SURFACE-WIDTH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-HEIGHT PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-PITCH PARENDECK2D::LOADED-IMG): 384 (SDL2:SURFACE-FORMAT PARENDECK2D::LOADED-IMG): #.(SB-SYS:INT-SAP #X7FFFDC3B8A10) (SDL2:SURFACE-FORMAT-FORMAT PARENDECK2D::LOADED-IMG): :BGR24
<DEBUG> [00:42:34] p2d filegNgwby (load-debug-images-and-dump-info load-img-and-dump-info) - PARENDECK2D::IMG: “assets/trc_tex_32bit_argb.bmp” (SDL2:SURFACE-WIDTH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-HEIGHT PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-PITCH PARENDECK2D::LOADED-IMG): 512 (SDL2:SURFACE-FORMAT PARENDECK2D::LOADED-IMG): #.(SB-SYS:INT-SAP #X7FFFDC3B8A10) (SDL2:SURFACE-FORMAT-FORMAT PARENDECK2D::LOADED-IMG): :RGBA8888
<DEBUG> [00:42:34] p2d filegNgwby (load-debug-images-and-dump-info load-img-and-dump-info) - PARENDECK2D::IMG: “assets/trc_tex_32bit_xrgb.bmp” (SDL2:SURFACE-WIDTH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-HEIGHT PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-PITCH PARENDECK2D::LOADED-IMG): 512 (SDL2:SURFACE-FORMAT PARENDECK2D::LOADED-IMG): #.(SB-SYS:INT-SAP #X7FFFDC3B8A10) (SDL2:SURFACE-FORMAT-FORMAT PARENDECK2D::LOADED-IMG): :RGBX8888
<DEBUG> [00:42:34] p2d filegNgwby (load-debug-images-and-dump-info load-img-and-dump-info) - PARENDECK2D::IMG: “assets/trc_tex_24bit.png” (SDL2:SURFACE-WIDTH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-HEIGHT PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-PITCH PARENDECK2D::LOADED-IMG): 384 (SDL2:SURFACE-FORMAT PARENDECK2D::LOADED-IMG): #.(SB-SYS:INT-SAP #X7FFFDC3D8390) (SDL2:SURFACE-FORMAT-FORMAT PARENDECK2D::LOADED-IMG): :RGB24
<DEBUG> [00:42:34] p2d filegNgwby (load-debug-images-and-dump-info load-img-and-dump-info) - PARENDECK2D::IMG: “assets/trc_tex_32bit.png” (SDL2:SURFACE-WIDTH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-HEIGHT PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-PITCH PARENDECK2D::LOADED-IMG): 512 (SDL2:SURFACE-FORMAT PARENDECK2D::LOADED-IMG): #.(SB-SYS:INT-SAP #X7FFFDC3FD0E0) (SDL2:SURFACE-FORMAT-FORMAT PARENDECK2D::LOADED-IMG): :ABGR8888
<DEBUG> [00:42:34] p2d filegNgwby (load-debug-images-and-dump-info load-img-and-dump-info) - PARENDECK2D::IMG: “assets/trc_tex_lzw.tiff” (SDL2:SURFACE-WIDTH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-HEIGHT PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-PITCH PARENDECK2D::LOADED-IMG): 512 (SDL2:SURFACE-FORMAT PARENDECK2D::LOADED-IMG): #.(SB-SYS:INT-SAP #X7FFFDC3FD0E0) (SDL2:SURFACE-FORMAT-FORMAT PARENDECK2D::LOADED-IMG): :ABGR8888
<DEBUG> [00:42:34] p2d filegNgwby (load-debug-images-and-dump-info load-img-and-dump-info) - PARENDECK2D::IMG: “assets/trc_tex_packed.tiff” (SDL2:SURFACE-WIDTH PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-HEIGHT PARENDECK2D::LOADED-IMG): 128 (SDL2:SURFACE-PITCH PARENDECK2D::LOADED-IMG): 512 (SDL2:SURFACE-FORMAT PARENDECK2D::LOADED-IMG): #.(SB-SYS:INT-SAP #X7FFFDC3FD0E0) (SDL2:SURFACE-FORMAT-FORMAT PARENDECK2D::LOADED-IMG): :ABGR8888
Surprisingly difficult to find.
- (TTF) Bitstream Vera fonts
- (bitmap) Good Neighbors pixel font; link via davexunit (author of Chickadee)
Important observation - if you change an inlined function, best recompile the whole project; otherwise, Quicklisp / ASDF won’t pick it up by itself and you may end up with outdated code here and there, doing weird stuff.
(asdf:load-system :parendeck2d :force t :force-not t)