Refreshing the fake screen is still a heavyweight operation. Double-buffering
makes it less obvious but doesn't actually reduce the amount of work. We
need to ensure that we do enough work between refreshes to make them economic.
Font rendering now happens off the real screen, which provides the effect
of double-buffering.
Apps can now also use convert-graphemes-to-pixels for more traditional
double-buffering.
Two interesting things:
- We don't really need double-buffering for rendering the screen on the
sandbox as a progress indicator. Everything else is untouched, and render-screen
should be doing that as well.
- Rendering just pixels of the fake screen is buttery smooth. It's the
_graphemes_ that are slowing things down. Even though there's so many
fewer of them!
As a result, drawing the fake screen less frequently in `evaluate` doesn't
actually help with flicker. Even though it'll make the debug cycle shorter.
So my current plan is to attack flicker in isolation before I mess with
the render frequency.
In this commit I optimized away the cursor handling. Still doesn't seem
to be helping. In fact it actually seems _worse_.
Rename cells containing screens to screen vars because of the ambiguity
that each grapheme in fake screens is represented by a type screen-cell.
While we're at it, we also analogously rename keyboard vars.
Needed because we don't yet have a primitive in the shell to truncate/round
non-integers to integers.
Before:
(nth (/ 31 10) # we don't have float literals yet
'(1 2 3 4))
=> NULL
..with an unpleasant abort likely later on.
Really the correct thing to do is ensure none of my primitives ever returns
NULL. Start with car/cdr.
After all that, I'm not sure this is the desired behavior. If a function
defines multiple bindings, we shouldn't rename all their keys. So how to
choose?
Perhaps it's not so bad to have "symlinks" in this "file system". To unlink
two bindings you now need to define one of them in the sandbox.
All the refactoring is still useful, though.
Save a single trace to potentially multiple globals just like a gap buffer
(if say we have a single let binding defining multiple functions).
I don't have a strong use for this yet, but it seems cleaner. Maybe it's
redundant or wrong.
Inline a function by patching a few variable names. I don't even have to
worry about `return` statements if there's a single call and it's in tail
position in the caller.
We don't have support for browsing them yet. Just errors for now, which
should only be a line or two. Larger traces might be useful for inspecting
results of macroexpansion.
It turns out there's another problem, and it predates the ability to create
new definitions:
ctrl-s triggers a call to `evaluate`, which inserts a new definition
into globals. which has a null gap buffer.
All this happens long before the new code in this commit, resulting in a
null gap buffer by the time we get to word-at-cursor.
Which in turn happens because we perform a raw `evaluate`, which doesn't
update the gap buffer like `run` does (using `maybe-stash-gap-buffer-to-global`).
And arguably `evaluate` shouldn't mess with the gap buffer. Gap buffers
are a UI concern.
The hardest version of this immediate scenario: It's unclear how to guarantee
that every definition have a gap buffer, when two definitions may share
one (closures sharing a lexical environment).
New plan:
- improve the logic for detecting definitions. Looking at the outermost
layer isn't enough. And a single expression can create multiple definitions.
- extract a helper to attach a single gap buffer to multiple definitions.
- have the UI detect conflicts in gap buffers and prompt the user for
a decision if a different gap buffer already exists for a definition.
I wrote a comment about how some code was not covered by tests, and then
promptly forgot what it was for. This is why we need tests.
Now the hack is gone.
It turns out (bowboard screen 128) on a real screen massively slowed down
and ran out of memory since commit e2ab1b30b1 on May 19. The culprit was
these changes, which created memory allocations for a new trace on every
recursive call.
I originally had some vague desire to isolate these calls from the user-visible
trace. That's expensive enough that I'll wait until it becomes a concern
before trying to isolate again.
It turns out I have a problem with trace depth somewhere which I just wasn't
noticing before. Running certain sandboxes (line; maybe loops?) twice was
causing traces to no longer start at depth 1, which implies that they weren't
terminating at depth 1. This became a lot more obvious since I instituted
a max-depth.