summaryrefslogtreecommitdiff
path: root/src/modules
AgeCommit message (Collapse)Author
2023-01-11src/modules/{stars,plato}: stream tapped variablesVito Caputo
Now that til_stream_t is implemented, let's wire up the taps. Note that nothing actually creates the stream and puts it in the fragment yet, so stream is still always NULL for these effectively turning this into a NOP.
2023-01-10*: introduce paths for module contextsVito Caputo
There needs to be a way to address module context instances by name externally, in a manner complementary to settings and taps. This commit adds a string-based path to til_module_context_t, and modifies til_module_create_context() to accept a parent path which is then concatenated with the name of the module to produce the module instance's new path. The name separator used in the paths is '/' just like filesystem paths, but these paths have no relationship to filesystems or files. The root module context creation in rototiller's main simply passes "" as the parent path, resulting in a "/" root as one would expect. There are some obvious complications introduced here however: - checkers in particular creates a context per cpu, simply using the same seed and setup to try make the contexts identical at the same ticks value. With this commit I'm simply passing the incoming path as the parent for creating those contexts, but it's unclear to me if that will work OK. With an eye towards taps deriving their parent path from the context path, I guess these taps would all get the same parent and hash to the same value despite being duplicated. Maybe it Just Works, but one thing is clear - there won't be any way to address the per-cpu taps as-is. Maybe that's desirable though, there's probably not much use in trying to control the taps at the CPU granularity. - when the recursive settings stuff lands, it should bring along the ability to explicitly name settings blocks. Those names should override the module name in constructing the path. I've noted as such in the code. - these paths probably need to be hashed @ initialization time so there needs to be a hash function added to til, and a hash value accompanying the name in the module context. It'd be dumb to keep recomputing the hash when these paths get used for hash table lookups multiple times per frame... there's probably more I'm forgetting right now, but this seems like a good first step. fixup root path
2023-01-10modules/stars: add taps for some varsVito Caputo
Wiring up some minimal taps to see how this will work... This only initializes the taps and changes the render to access the rates indirectly via the tapped pointers.
2023-01-10modules/plato: add taps for {spin,orbit}_rate varsVito Caputo
Wiring up some minimal taps to see how this will work... This only initializes the taps and changes the render to access the rates indirectly via the tapped pointers.
2022-11-12modules/compose: more robust texture preservationVito Caputo
The existing code only really cared about preserving the incoming texture on behalf of the caller when the compose code itself was doing something with the texture. But there's scenarios where the underlying module being rendered (or its descendants) might play with the texture, and in such a situation when the outer compose wasn't involving a texture it'd let the descandants installed texture leak out to the callers making the texture apply more than one'd expect. Arguably none of the modules should be missing restoration of the incoming texture after installing+rendering with their own. But with this change in place, compose will clean up after nested modules leaking their texture up.
2022-11-09modules/strobe: force flash even at low FPSPhilip J Freeman
At low framerates, the strobe module timer can expire from frame to frame. This has the effect of appearing "always on." This condition was obscuring output in compose mode. #BirdwalkCommit #WinterStormWarning
2022-09-05modules/roto: move tables init to context createVito Caputo
Mechanical rearrangement, but ultimately there probably needs to be an initialize function added to til_module_t. With all the threading chaos going on, this approach to implicit initialization with a static flag is racy without using atomics. For now it's probably marginally better to do this in context create vs. prepare frame. Context creates *tend* to happen in single-threaded phases of execution, and infrequently. Prepare frame is a serialized phase of the rendering for a given context, but there can be many contexts in-flight simultaneously now with all the forms of compositing happening, sometimes from multiple threads. So that assumption no longer holds...
2022-09-05modules/blinds: use til_ticks_to_rads(ticks)Vito Caputo
This gets rid of the static accumulator hack used for the blinds phase.
2022-09-04modules/strobe: add rudimentary strobe light moduleVito Caputo
After reading about the Dreamachine[0], I wanted to experience this phenomenon. The javascript-based web implementations struggled to hold a steady 10Hz rate and would flicker like crazy, so here we are. Only setting right now is period=float_seconds, defaults to .1 for 10Hz. One limitation in the current implementation is when the frame rate can't keep up with the period the strobe will just stick on without ever going off, because the period will always be expired. There should probably be a setting to force turning off for at least one frame when it can't keep up. [0] https://en.wikipedia.org/wiki/Dreamachine
2022-09-04til: fixup til_fb_fragment_t.texture fragmentingVito Caputo
Until now when fragmenting with a texture present the texture pointer was simply copied through to the new logical fragment. The problem with that is when sampling pixels from the texture in a nested frame scenario, the locations didn't align with the placement of the logical fragment. With this change when the incoming fragment has a texture, the output fragment gets some uninitialized memory attached in the outgoing fragment's texture pointer. Then the fragmenter is expected to do the same populating of res_fragment->texture it already did for res_fragment, just relative to fragment->texture->{buf,stride,pitch} etc. It's a bit hairy/janky because til_fb_fragment_t.texture is just a pointer to another til_fb_fragment_t. So the ephemeral/logical fragments fragmenting/tiling produces which tend to just be sitting on the stack need to get another til_fb_fragment_t instance somewhere and made available at the ephemeral til_fb_fragment_t's .texture member. We don't want to be allocating and freeing these things constantly, so for now I'm just ad-hoc stowing the pointer of an adjacent on-stack texture fragment in the .texture member when the incoming fragment has a texture. But this is gross because the rest of the fragment contents don't get initialized _at_all_, and currently if the incoming fragment has no texture the res_fragment->texture member isn't even initialized. The fragmenters aren't really supposed to be expecting anything sensible in *res_fragment, but now we're making use of res_fragment->texture *if* fragment->texture is set. This is just gross. So there's a bunch of asserts sprinkled around to help police this fragility for now, but if someone writes new fragmenters there's a good chance this will trip them up.
2022-08-11modules/plato: add some rudimentary settingsVito Caputo
Just looking to spice up plato a bit. It seems to make a lot of appearances in rtv, or is just one of those highly visible things when it's participating. Way too monotonous as-is.
2022-08-07modules/compose: add moire as a texture moduleVito Caputo
moire makes for an interesting texture, esp. mixed with itself: --seed=0x62f00a7b --module=compose,layers=julia:moire:drizzle,texture=moire
2022-08-07modules/drizzle: add a mapped overlay styleVito Caputo
this introduces a style= setting with values: style=mask simple alpha mask overlay style=map displacement mapped overlay I might add a lighting option for the style=map mode with a moving light source or something like that, but it's already pretty slow as-is. This is mostly just for more testing of the snapshotting, but there's some interesting compositions enabled like: module=compose,layers=submit:moire:drizzle or just moire:drizzle, when style=map happens.
2022-08-07modules/drizzle: experimenting with the new snapshottingVito Caputo
This arguably doesn't require snapshotting to work, since it's doing a rudimentary in-place single pixel alpha-blended replacement using a single pixel at the same location. But the moment it startus using multiple adjacent super-samples, the snapshot becomes necessary. If it gets further complicated with displacements and maybe bump-mapping or something, then the samples can become quite distant from the pixel being written, spreading out into neighboring fragments being rendered simultaneously etc. These are all cause for snapshotting... For now though it's a very simple implementation that at least makes drizzle overlayable, while also providing a smoke test for the new snapshotting functionality.
2022-08-07til: til_fb_fragment_t **fragment_ptr all the thingsVito Caputo
Preparatory commit for enabling cloneable/swappable fragments There's an outstanding issue with the til_fb_page_t submission, see comments. Doesn't matter for now since cloning doesn't happen yet, but will need to be addressed before they do.
2022-07-24modules/checkers: center unaligned checkers scenariosVito Caputo
This introduces a bespoke fragmenter for checkers. The generic til_fb tiler isn't concerned with aesthetics so it doesn't particularly care if clipped tiles are asymmetrically distributed. It worked fine to get checkers developed and working, but it's really unattractive to have the whole be off-centered when the checkers don't perfectly align with the frame size. There's some gross aspects like leaving the frame_{width,height} to be corrected at render time so render_fragment can access the incoming frame_width for cell state determination.
2022-07-24modules/plato: scale to frame sizeVito Caputo
quick and dirty fixup for proper use as checkers fill_module= note this thing already relies on _checked() put_pixel variant
2022-07-24modules/stars: more fast and nasty fragment clip to frame fixesVito Caputo
s/unchecked/checked/ and use frame dimensions, probably more fixes needed but this prevents crashing as checkers fill_module= at least.
2022-07-24modules/spiro: fast and dirty frame clipping fixupVito Caputo
Sorry but more s/unchecked/checked/ and now this seems to not crash when used as a checkers fill_module.
2022-07-24modules/shapes: fix up clipped fragment/frameVito Caputo
This is a first approximation of correct handling of arbitrarily clipped frames described by the incoming fragment. It's still relying on the _checked() put_pixel variants for clipping. That should probably be improved by constraining the loops to the clipped fragment edges.
2022-07-21modules/{compose,rtv}: s/prepare_frame/render_fragment/Vito Caputo
These modules have been doing their work in prepare_frame(), but aren't actually threaded modules and don't return a frame plan from prepare_frame() nor do they provide a render_fragment() to complement the prepare_frame(). The convention thus far has been that single-threaded modules just provide a render_fragment and by not providing a prepare_frame they will be executed serially. These two modules break the contract in a sense by using prepare_frame() without following up with render_fragment(). I'm not sure why it happened that way, maybe at one time prepare_frame() had access to some things that render_fragment() didn't. In any case, just make these use render_fragment() like any other simple non-threaded module would. This was actually causing a crash when n_cpus=1 because module_render_fragment() was assuming the prepare_frame() branch would include a render_fragment(). It should probably be asserting as such.
2022-07-20modules/pixbounce: s/rand/rand_r/Vito Caputo
Wire up seed via til_module_context.seed in the obvious manner, minimal change to the code, no functional difference besides giving pixbounces instances an isolated random seed state.
2022-07-20modules/meta2d: more rand()->rand_r() conversionsVito Caputo
Normalized all the randomizers to use til_module_context.seed while in here.
2022-07-20modules/checkers: one more rand/rand_r conversionVito Caputo
wired up to til_module_context.seed
2022-07-20modules/sparkler: s/rand/rand_r/ and wire up seedVito Caputo
This is a little contorted but not too bad. The input to particles_new() is just a const conf struct, so instead of passing in the seed value for particles_t to contain, a pointer to where the seed lives is passed in via the conf. This requires the caller to persist a seed somewhere outside the particles instance, but at least in rototiller we already have that conveniently in til_module_context_t.
2022-07-20modules/drizzle: switch to rand_r w/local seedVito Caputo
More obvious migrations to using the supplied seed
2022-07-20libs/din: pass seed to din_new()Vito Caputo
also update call sites in modules/{meta2d,swab} accordingly
2022-07-20modules/submit: wire up seed to randomizersVito Caputo
More plumbing seed to rand in the obvious way...
2022-07-20modules/swarm: wire up seed to various randomizersVito Caputo
Just plumbing seed down in the obvoius manner, this could probably be cleaned up a bit in the future.
2022-07-20modules/flui2d: fix clockstep value to match defaultVito Caputo
Due to how the values get matched as strings this extraneous 0 was interfering with FLUI2D_DEFAULT_CLOCKSTEP actually matching anything.
2022-07-18modules/rtv: s/rand/rand_r/Vito Caputo
just fixing up some vestigial rand() invocations to use the seed
2022-07-18til: wire seed up to til randomizersVito Caputo
til_setting_desc_t.random() and til_module_randomize_setup() now take seeds. Note they are not taking a pointer to a shared seed, but instead receive the seed by value. If a caller wishes the seed to evolve on every invocation into these functions, it should simply insert a rand_r(&seed) in producing the supplied seed value. Within a given randomizer, the seed evolves when appropriate. But isolating the effects by default seems appropriate, so callers can easily have determinism within their respective scope regardless of how much nested random use occurs.
2022-06-13modules/sparkler: plug longstanding chunker leakVito Caputo
Once upon a time this thing asserted the pinned chunks were empty, and that code is still sitting there commented out. But when it was commented out ages ago, to enable bulk freeing of chunkers without requiring unrefs of every allocation as an optimization, no code was added to free the pinned chunks. It's been easily ignored all this time since nobody really runs rototiller long enough to notice, but that's becoming less true now with how interesting something like: --module=rtv,layers=compose,duration=1,context_duration=1,snow_module=none is becoming... There are other leaks still, largely surrounding settings, but they are quite small. Eventually those will get tidied up as well.
2022-06-10modules/checkers: experimenting with fill modesVito Caputo
this introduces a color= setting syntax: color=#rrggbb color=0xrrggbb color=rrggbb where rrggbb is case-insensitive html-style hexadecimal also introduces a fill= setting: fill=color fill=sampled fill=textured fill=random fill=mixed sampled draws the color from the incoming fragment when layered, textured draws the pixels from the texture when available, random randomizes the choice from color,sampled,textured. mixed isn't implemented fully and is just aliased to random currently. The thinking for mixed is to allow specifying proportions for color,sampled,textured which would then be applied as weights when randomizing the selection from the three at every filled checker. the current implementation is just calling rand() when randomized, but should really be like the other dynamics in checkers with rate control and hash-based. and introduces a fill_module= setting: this is a first stab at employing other modules for filling the filled cells. Note since checkers is already a threaded module, the fill module context gets created per-cpu but with an n_cpus=1. This is kind of the first time module contexts are being rendered manifold for the same frame, and that's illuminating some shortcomings which needed to be dealt with. Some modules automatically advance a phase/T value on every render which gets persisted in their context struct. With how checkers is using contexts, it's desirable for multiple renders of the same context using the same ticks to produce the same output. So modules need to be more careful about time and determine "dt" (delta-time) values, and animate proportional to ticks elapsed. When ticks doesn't change between renders, dt is zero, and nothing should change. For now this is using a hard-coded list of modules to choose from, you specify the module by name or "none" for no fill_module (solid checker fill). ex: "fill_module=shapes" There's a need for something like fragment color and flag overrides to allow til_module_render() to be treated as more of a brush where the caller gets to specify what colors to use, or if texturing should be allowed. For now, when fill_module=$module is employed, the color determination stuff within checkers doesn't get applied. That will need to be fixed in the future.
2022-06-10til: introduce til_frame_plan_t and .cpu_affinityVito Caputo
modules/checkers w/fill_module=$module requires a consistent mapping of cpu to fragnum since it creates a per-cpu til_module_context_t for the fill_module. The existing implementation for threaded rendering maximizes performance by letting *any* scheduled to run thread advance fragnum atomically and render the acquired fragnum indiscriminately. A side effect of this is any given frame, even rendered by the same module, will have a random mapping of cpus/threads to fragnums. With this change, the simple til_module_t.prepare_frame() API of returning a bare fragmenter function is changed to instead return a "frame plan" in til_frame_plan_t. Right now til_frame_plan_t just contains the same fragmenter as before, but also has a .cpu_affinity member for setting if the frame requires a stable relationship of cpu/thread to fragnum. Setting .cpu_affinity should be avoided if unnecessary, and that is the default if you don't mention .cpu_affinity at all when initializing the plan in the ergonomic manner w/designated initializers. This is because the way .cpu_affinity is implemented will leave threads spinning while they poll for *their* next fragnum using atomic intrinsics. There's probably some room for improvement here, but this is good enough for now to get things working and correct.
2022-06-10modules/roto: drive from ticks, move palette to contetxtVito Caputo
The palette mutates across frames, on a context-specific schedule. Meaning the palette is per-context, so move it into roto_context_t. The phase also needs to be driven by ticks. And when ticks doesn't change in cases where the same context is rendered manifold in the same frame, the phase shouldn't move.
2022-06-10modules/plato: derive movement from delta ticksVito Caputo
This way if a given context gets rendered repeatedly for the same tick, no movement occurs, until ticks changes.
2022-06-10til: add ticks to til_module_context_tVito Caputo
Also wire this up to the til_module_context_new() helper and all its callers. This is in preparation for modules doing more correct delta-T derived animation.
2022-06-10modules/blinds: use til_fb_put_pixel_checked()Vito Caputo
While testing an experimental checkers w/fill_module=blinds with ASAN it became clear this module is making flawed assumptions about fragment->frame_{width,height} and fragment->{width,height} being equal. When used by checkers for filling cells, there are situations where the edge cell fragments need to describe a frame slightly larger than the drawn area, because the cell size doesn't align perfectly to the overall window/screen dimensions. So in these cases the synthesized frame will still be a full cell's dimensions while the width,height serve to clip within that area. If modules aren't properly clipping their rendering, instead just using frame_{width,height}, then they will have to use the _checked() variants to ensure clipping occurs properly on a per-pixel (slower) basis.
2022-06-10modules/montage: minor fixupsVito Caputo
Contexts aren't void* anymore, and free the contexts array too on failure.
2022-06-10modules/montage: remove vestigial unused variableVito Caputo
This initializer could perform an out-of-bounds read since it occurs before the n_modules bounds check. Since the variable isn't even being used anymore just get rid of this. Also found via ASAN.
2022-06-10modules/pixbounce: use til_fb_put_pixel_checked()Vito Caputo
While testing a checkers change that fills cells using other modules, ASAN kept tripping on pixbounce: ==147817==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7fc78a31c10c at pc 0x55b30cd406e2 bp 0x7fc790afd0d0 sp 0x7fc790afd0c8 WRITE of size 4 at 0x7fc78a31c10c thread T2 #0 0x55b30cd406e1 in til_fb_fragment_put_pixel_unchecked pixbounce.c #1 0x55b30cd3f8ae in pixbounce_render_fragment pixbounce.c #2 0x55b30cd1dffb in module_render_fragment til.c #3 0x55b30cd1d989 in til_module_render (/home/foo/src/rototiller/build/src/rototiller+0x134989) #4 0x55b30cd22534 in checkers_render_fragment checkers.c #5 0x55b30cd14681 in thread_func til_threads.c #6 0x7fc792b3d5c1 in start_thread pthread_create.c #7 0x7fc792bc2583 in __clone (/usr/lib/libc.so.6+0x112583) 0x7fc78a31c10c is located 2276 bytes to the right of 1228840-byte region [0x7fc78a1ef800,0x7fc78a31b828) allocated by thread T0 here: #0 0x55b30cccf219 in __interceptor_malloc (/home/foo/src/rototiller/build/src/rototiller+0xe6219) #1 0x7fc792d0e528 (/usr/lib/libSDL2-2.0.so.0+0x39528) Thread T2 created by T0 here: #0 0x55b30cc3cfa8 in pthread_create (/home/foo/src/rototiller/build/src/rototiller+0x53fa8) #1 0x55b30cd13fff in til_threads_create (/home/foo/src/rototiller/build/src/rototiller+0x12afff) #2 0x55b30cd1d573 in til_init (/home/foo/src/rototiller/build/src/rototiller+0x134573) #3 0x55b30cd08f6c in main (/home/foo/src/rototiller/build/src/rototiller+0x11ff6c) #4 0x7fc792add30f in __libc_start_call_main libc-start.c SUMMARY: AddressSanitizer: heap-buffer-overflow pixbounce.c in til_fb_fragment_put_pixel_unchecked Shadow bytes around the buggy address: 0x0ff97145b7d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0ff97145b7e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0ff97145b7f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0ff97145b800: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0ff97145b810: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =>0x0ff97145b820: fa[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0ff97145b830: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0ff97145b840: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0ff97145b850: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0ff97145b860: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0ff97145b870: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==147817==ABORTING --- Rather than spend time digging into pixbounce's arithmetic, just using the checked variant for now.
2022-06-07modules/moire: implement rudimentary moire moduleVito Caputo
This introduces a very naive unoptimized moire interference pattern module, it's rather slow complete with a sqrtf() per pixel per center.
2022-05-29*: pivot to til_module_context_tVito Caputo
- modules now allocate their contexts using til_module_context_new() instead of [cm]alloc(). - modules simply embed til_module_context_t at the start of their respective private context structs, if they do anything with contexts - modules that do nothing with contexts (lack a create_context() method), will now *always* get a til_module_context_t supplied to their other methods regardless of their create_context() presence. So even if you don't have a create_context(), your prepare_frame() and/or render_fragment() methods can still access seed and n_cpus from within the til_module_context_t passed in as context, *always*. - modules that *do* have a create_context() method, implying they have their own private context type, will have to cast the til_module_context_t supplied to the other methods to their private context type. By embedding the til_module_context_t at the *start* of their private context struct, a simple cast is all that's needed. If it's placed somewhere else, more annoying container_of() style macros are needed - this is strongly discouraged, just put it at the start of struct. - til_module_create_context() now takes n_cpus, which may be set to 0 for automatically assigning the number of threads in its place. Any non-zero value is treated as an explicit n_cpus, primarily intended for setting it to 1 for single-threaded contexts necessary when embedded within an already-threaded composite module. - modules like montage which open-coded a single-threaded render are now using the same til_module_render_fragment() as everything else, since til_module_create_context() is accepting n_cpus. - til_module_create_context() now produces a real type, not void *, that is til_module_context_t *. All the other module context functions now operate on this type, and since til_module_context_t.module tracks the module this context relates to, those functions no longer require both the module and context be passed in. This is especially helpful for compositing modules which do a lot of module context creation and destruction; the module handle is now only needed to create the contexts. Everything else operating on that context only needs the single context pointer, not module+context pairs, which was unnecessarily annoying. - if your module's context can be destroyed with a simple free(), without any deeper knowledge or freeing of nested pointers, you can now simply omit destroy_context() altogether. When destroy_context() is missing, til_module_context_free() will automatically use libc's free() on the pointer returned from your create_context() (or on the pointer that was automatically created if you omitted create_context() too, for the bare til_module_context_t that got created on your behalf anyways). For the most part, these changes don't affect module creation. In some ways this eases module creation by making it more convenient access seed and n_cpus if you had no further requirement for a context struct. In other ways it's slightly annoying to have to do type-casts when you're working with your own context type, since before it was all void* and didn't require casts when assigning to your typed context variables. The elimination for requiring a destroy_context() method in simple free() of private context scenarios removes some boilerplate in simple cases. I think it's a wash for module writers, or maybe a slight win for the simple cases.
2022-05-25*: normalize on all case-insensitive comparisonsVito Caputo
I don't think rototiller is an appropriate place for being so uncooperative, if someone gets the case wrong anywhere just make it work. We should avoid making different things so subtly different that case alone is the distinction anyways, so I don't see this creating any future namespace collision problems.
2022-05-25modules/shapes: update TODO commentVito Caputo
A bunch of these have just been done
2022-05-25modules/shapes: slow spins down a bitVito Caputo
I didn't like the too fast spins where you can't even really get a read on what's going on in the shape. Suspect this will get tweaked more in the future...
2022-05-25modules/shapes: introduce pinch settingVito Caputo
This is kind of experimental, not sure how I feel about it. pinch=0..1 with a bunch of fractions, 0 disables it. pinch_spin=-1..1 same as spin= pinches=1..10 number of pinches, which come in pairs This applies to all the current shapes, for a tour: --module=rtv,channels=shapes,duration=2,context_duration=2,snow_module=none --defaults I think the speeds might go to high atm, I kind of liked the slower spins before all this more.
2022-05-24modules/shapes: fixup indentation of star+pinwheelVito Caputo
woops
2022-05-24modules/shapes: add scale settingVito Caputo
scale=1-.5 with a few other fractions in between Mostly added for the sake of rtv/compose where modules currently always go full-frame, which can be really annoying sometimes for shapes without any variety in the scale. When checkers starts filling cells using modules like shapes, it'll be interesting to see how this fares there since it'll probably be randomizing the settings too. At least floored at 50% should still produce something legible in /most/ of the checkers cell sizes. Definitely won't look like anything in the smaller end of the checkers sizes though... hrm.
© All Rights Reserved