Just a small overview of some of the logic behind creating Lynx games/demos, from the top of my head:
All graphics on the Lynx are sprites. Even the backgrounds. Sprites can be upto 511 pixels wide at a height only limited by memory. You could write directly to the display buffer using the CPU (at four bits per pixel for sixteen colors), but this will be much slower for most purposes.
Your sprites can be anywhere in memory. Sprites have a small header of data telling them where they should be positioned within a 32K x 32K pixel area. This header also contains the settings for x/y scaling, tapering, sheering, collision settings, x/y mirroring, positioning from top-left or another position, transparent color, RLE compression* and color remapping.
In this header you also set a pointer to the next sprite to be drawn, creating a linked list of sprites only limited by memory. In this header you also set a pointer to the actual pixel image of your sprite.
Then you tell the graphics chip where the top-left position of your display in this 32K x 32K 'world' currently is (by setting an x and y position hardware register). By changing this position each frame, you can scroll the display in all directions. After that you tell the graphics chip to start drawing your sprites (after you have pointed it to the first sprite in your list). It will start with the first sprite and then draw all the other sprites in the order your list is in.
So it will draw your sprites back to front, each next sprite in the list on top of the previous (which you will only notice if they are at overlapping positions ofcourse).
A way to make sure everything moves fluidly is to use the vertical blanking interrupt. This will call the function you point it to every time the display has finished displaying one frame. If you change all your sprite positions and anything else you want to animate in that function and then draw your sprites, things will move smoothly (if it is able to handle all that in 1/60th of a second for a single frame).
To support lower or unstable framerates, have more time to draw your frames and avoid flickering, you can use double buffering. This means you have one display buffer that is currently being shown on the display, while your sprites are being drawn to another display buffer that isn't visible yet. Then when the vertical blank interrupt calls your function and your sprites have all been drawn, you can swap both buffers, showing your newly drawn buffer on the display.
Apart from the vertical blank interrupt, there is also a horizontal blank interrupt. This is called every time a horizontal line (scanline) on the display has been displayed. The time you have here to do something before the next is displayed is much shorter than with the vertical blank interrupt. A common function to use with this interrupt is to change the color of the background for each scanline, this way you can make smooth gradients for a sky or ground (like in Shadow of the Beast and Roadblasters).
Changing the whole palette (or any amount of it) at a certain scanline (vertical position) is also possible, but there is not enough time to do this. A solution is to show one or two empty lines while you are updating your palette. The graphics below those lines can then be displayed using this different palette. IIRC Roadblasters does this.
Drawing vector lines and filled polygons on the Lynx is all done by scaling, tapering and sheering one or two single pixel sprites. There is some math involved in converting the vector coordinates to these scale, taper and sheer settings.
*RLE compression stands for Run Lenght Encoding. A very simple way of making your sprite data smaller in memory. The way this works is you set how many pixels of the following color the sprite engine should draw horizontally. So for instance you would tell it to draw 64 red pixels, followed by 32 blue pixels, followed by 50 green pixels. Which would be much smaller than telling it which pixel color to draw 146 times.
This only works well for sprites that have large areas of the same color, like cartoony looking sprites for instance.
If your sprite has lots of small detail, meaning the color changes almost every pixel horizontally, RLE compression will actually make the data larger than when not using it. In this case you would be saying: Draw an orange pixel one time, then draw a white pixel two times, draw a blue pixel one time, which would be really inefficient.
I hope this information will be useful to someone. I only programmed the Lynx in assembly, which is really close to the hardware. Programming it in C will probably have some different logic here and there.