Greyscale for arduboy

I do not believe it is misleading for two reasons: first because I explain in the source code how this work and the fact that flickering cannot be avoided (without VSYNC). It’s just less flickering with less CPU usage (after manual calibration). And second:

because this is exactly what the controller does (and, from memory, I believe I even wrote that in the source code). Also, even when the controller swaps the buffer during vsync, flickering doesn’t go away completely because without VSYNC you cannot guarantee you will send the commands to swap the screen at the right time resulting in some frames staying visible longer then they should.

I know this and I have posted on this topic before. I am one of those annoying “it can’t be done without VSYNC” persons. What I published here is a low-overhead full screen (albeit half-vertical-resolution) gray technique based on double buffering on the display controller that results in less flickering compared to relying on the CPU pushing updates while racing display refresh.

FYI I never claimed the decoupling is only possible done this way. What I said is that the decoupling can be done with low CPU usage. And, if you involve the CPU, the frequency and intensity of gliches is likely to increase.

Phew I did it again: I wrote too much! :stuck_out_tongue_closed_eyes:

Did anyone actually try the demo? While far from perfect or convenient (because of the calibration step) I think it may be usable especially when the screen is not zoomed (i.e. half-height) because even without calibration the flickering is not very annoying (the controller is refreshing the display twice as fast because there is half the number of scanlines).

1 Like

How is the buffer different for zoom mode? Would it not just be a 128x32 buffer instead?

Yes, but it’s not that bad because the entire screen is swapped by the controller during vsync

If true (which seems possible) that’s pretty cool, but too bad you’re stuck with 32 pixels of vertical resolution, seems like a big loss.

In pixel it’s 128x32 but with 2 bits per pixel there are different ways of organizing the buffers. The demo is using two 128x4 bytes buffers. Given a gray level enconded as two bits b1b0, buffer0 holds the value of b0 for all pixels and buffer1 holds the value of b1 for all the pixels. Grayscale drawing functions need to take that into account. Note that you could use existing functions to draw the same primitive twice in two different buffers but that wasteful because coordinate to offset computing would happen twice while a layout-aware function can find the position in the second buffer by adding a constant offset.

Conversely if the grayscale level were to be encoded in adjacent bits into the same byte (4 pixels per byte) both the SPI transfer function (because it would have to unpack/select the bits) and grascale drawing functions would have to be designed to support the specific layout.

Yes, it feels that way to me too. WHich reminded me (if memory serves) that the Commodor 64 had a multicolor sprite mode that resulted in sprites with half of the (horizontal) px count.

1 Like

If speed mattered the only way to do this would be separate sprites and buffers. (as you say you’re doing already) One benefit being you don’t necessarily need any new drawing code. Bit-wise operations (shifting) on AVR are ridiculously slow. Mixing the buffers would require a lot of additional effort at render-time to tear the buffers back apart and render just half the content.

Of course in a lot of cases speed doesn’t matter so much.

Really impressive demo. After manual calibration the flicker was greatly reduced. Wild to see gray work so well on Arduboy :smiley:

2 Likes

I just had a sorta-crazy idea. A lot of time has been spend trying to “dial in” the delay so it’s “just right”… but we know that’s impossible without a FR pin… and some games have done well just picking a static timer value and running with it (and accepting some flicker).

I wonder what it would look like if dialed it in but then ran with a random timer offset… so if the “perfect” value was say 16ms… then you render between 15.75 and 16.26ms, randomly… or non-randomly in some repeating pattern… I wonder if that would produce a “nicer” flicker pattern.

Instead of the “scan line” flying by (quickly or slowly) this would have the effect of randomizing it’s appearance - for better or worse.

I think I made a suggestion to this effect, but my solution was to allow the user to fiddle with the vsync until they were happy with it. Randomizing it would theoretically give a better “perception” of the tear, I think. Give it a try and let us know! :slight_smile:

Probably won’t find the time, but if someone else did I’d be happy to review the code and help think about if it was actually doing what they thought it was or not.

Yeah, the idea is you’d still let them “fiddle” but once you got it tuned in you’d switch to “exactly what you tuned +/- random offset” mode.

It might not really look that different from “slightly off tuned” (which is how Sensitive looked to me).

It seems Thumby users have come up with a (better?) method to keep the SSD1306 in sync. Technical details are interesting…

2 Likes

Amusingly

The Thumby uses the SSD1306 display driver chip. According to some people in the Arduboy community, some versions of this chip have a “hidden pin” called FR, that allows for perfect synchronisation.

We have reached the stage where the Arduboy forum is citing a Thumby project that cites the Arduboy forum. The circle is complete!

2 Likes

I do not believe the technique used by Thumby is applicable to the Arduboy :smiling_face_with_tear: (an explanation follows), nevertheless I am thinking of trying it to confirm.

The technique relies on setting up unused (off-screen) lines above and below the on-screen lines. This is possible on Thumby because only 40 of the 64 lines are used while the Arduboy uses all 64 lines. So I would expect the Arduboy to:

  1. End up with a mostly solid always-on first line (or last line, if the controller is setup to refresh bottom-up instead of top-down). This happens because the line used to lock and sync the refresh is visible on the Arduboy.
  2. Maybe show more flickering than Thumby at the top or bottom of the screen. This may happen because the command used to “lock” the refresh line and re-sync has to happen in a much narrower time-window on the Arduboy. i.e. during T(front porch) + T(back porch) instead of T(front porch) + T(back porch) + T(offscreen lines).

EDIT: On second thoughts brightness (or contrast) could be set to zero while locking the scanline or the vertical timing settings could be updated so that the locked line is in a region where the controller doesn’t allow it to drive the display. So many things to try … :crazy_face:

3 Likes

They seemed to dig deep into the control codes… hopefully there’s something of value there? Their source code is well commented.

No dice. I am not 100% done with testing but I can confirm the appearence of (the expected) artifacts on screen. One of the artifacts presents itself as a very bright bottom row on the display which can likely damage that specific row of OLEDs. This approach (on the Arduboy’s full size screen) is IMO worse than the existing solutions, both visually and in terms of CPU usage.

@dxb Picking your brain here because you understand this method much better than I:

Could a grayscale picture be achieved using this method with 8px tall black “anamorphic bars” on the top and bottom? Like using +8px vertical scroll and the bottom 16ish rows for the timing variation zone with all zeros written to the controller buffer for those rows. Then usable grayscale screen is 128x48.

Alternatively, 16px wide bars on the sides with +16px horizontal scroll and vertical addressing mode, with a 96x64 usable space.

1 Like

Hi, short answer is no. The reason is that in order to avoid artifacts the artifacts have to occur off-screen. Always-off bars would not be enough because those lines are still driven by the controller. The Thumby has a smaller screen so the lines where the glitches occur are not visible because they are physically not connected to the display.

If you are ok with half-screen grayscale (top, center, bottom or streched) then the method implemented in this demo works fairly well and is easy on the CPU.

1 Like

I feel we are quitting too easily… we cannot ‘lose’ to the new kids! :sweat_smile:

lol You can swap out the arduboy’s screen with a smaller one and it will work! :stuck_out_tongue_winking_eye:

1 Like

If the manufacturer would only bring out the pin, it’s on the controller and there is an available pin on the fpc. I asked, for whatever reason they can’t be bothered.

I’ve speculated before that they do this on purpose to just sell more grayscale displays.

After some experimenting – as long as controller RAM bits for the would-be off-screen pixels are kept cleared (sacrificing some screen space), I think we can adapt this approach!

Here’s an attempt.

Three variations selected by the macro GRAYSCALE_OPTION:

  • 0: 4-level in 2 frames using contrast (slight noise at border between 01 and 10)
  • 1: 4-level in 3 frames (slight flicker due to lower refresh rate)
  • 2: 3-level in 2 frames (no visual artifacts for me but only 3 levels)

It currently uses a timer for precise frame timing but I don’t think this is necessary. The usable area is cut to 128x55 pixels (the last page and the first row have to be kept zeroed out) and requires 1792 bytes to buffer, an increase of 768 bytes. If starved for RAM you could probably pre-render the appropriate plane each frame.

I was hoping to be able to sacrifice columns instead of rows to have a nicer aspect ratio left to work with. Unfortunately I don’t think this can work… I’ve learned the controller only drives entire rows so this approach needs extra rows to sacrifice for the timing variation.

2 Likes

Great work. I’m glad you proved me wrong. My test code isn’t using a timer and the result is far cry from yours.

EDIT 1:

FYI the acceptable range for BLAH on my Arduboy is [406, 443] using GRAYSCALE_OPTION 0 and the default Fosc setting (see 0xD5 command). Outside that range flickering, or other undesirable artifacts appear.

Hopefully there is a subset of values for BLAH that works OK for most Arduboys out there.

It is possible that setting Fosc to the maximum allowed value using 0xD5, 0xF0 will result in less dotclock jitter.

EDIT 2:

Adding 0x7C (see ‘10.1.6 Set Display Start Line’ in the SSD1306 datasheet) to SETUP_CMDS and disabling wiping of the first scan-line centers the usable area vertically in the screen and makes the first line of the frame buffer available for use (usable area: 128x56 px).

static uint8_t const SETUP_CMDS[] PROGMEM =
    {
        0x22, 0, FBR - 1,
        0x91, 24,
        0x7C,
#if GRAYSCALE_OPTION == 0
        // slight boost to phases 1,2 improves flickering at 01,10 neighboring pixels
        0xD9, 0x33,
#endif

EDIT 3:

On my Arduboy, using both your original gist and one with my vertical alignment change, I am observing an anomalous burst (1 or more subsequent frames) every so often (approx. every 1 minute or so). The glitch looks like a black (partial?) frame to the naked eye. I wish I had a camera with a high enough frame rate to see what is going on exactly.

2 Likes