On a CPC CRTC registers can be used to do double buffering.
It is possible to hardware scroll the CRTC to any MODE 1 character cell both horizontally and vertically (ie. 8x8 MODE 1 pixels). This is easy to do and was used by a number of games. The CRTC registers 12 and 13 specify the screen base address, and can be set to point to any even memory address in the first 2048 bytes of each bank of 16K in the first 64K of memory. This allows both double buffering (by switching between say #4000 and #C000) and coarse scrolling (#C002, #C004 ... #C7FE).
The CRTC(s) are actually capable of performing some other special effects and hardware scrolling by using some clever techniques which require very accurate timing in order to adjust the CRTC registers at exactly the right point in each frame (usually even more accurately than required for split screen or mode effects). The only commercial game I know of that used this technique properly was ZTB (Mission Genocide). It does pixel accurate vertical hardware scrolling. This is achieved by a combination of modifying the above screen base registers (12 and 13), and by adjusting the vertical total adjust register (register 5) twice per frame.
There are strict rules which must be adhered to in order to create a steady display when tampering with the vertical total and vertical total adjust registers. They affect the total number of pixel rows displayed in each frame, and this value must be within a certain threshold or the screen will roll as if the monitor had the VHOLD out. The threshold values are somewhere between a total of 280 and 340 scan lines per frame, but this depends a lot on where the user has their VHOLD set on the monitor. A good rule to adhere to here is to design the display in order to get exactly 312 scan lines per frame, exactly as the standard PAL CRTC settings do. This way the user should have their monitor set up so the screen doesn't roll when they're using it normally.
Vertical pixel accurate hardware scrolls automatically require that the screen be split into at least two separate areas, each with complementary vertical total adjust values. This means you can easily achieve a static (non-scrolling) region either above or below the scrolling region (below is much simpler). The VTA register can be set between 0 and 31, but values above 8 behave differently on different CRTC's (some repeat the last character line, others continue incrementing the character line).
Fine horizontal hardware scrolling is much more difficult to achieve. It is based on the way the monitor handles the width of the horizontal sync signal (CRTC register 3) coming from the CRTC (which gets modified by the Gate Array). Basically, by reducing the width of the horizontal sync period by one (MODE 1) character, the screen will shift by half a (MODE 1) character. This allows single MODE 1 character scrolling. The exact timing of the HSYNC change is important, and will cause the monitor to bend the display slightly for a number of pixel rows (scan lines).
It is possible to achieve a finer horizontal scroll than half a character by using quarter character (one MODE 0 pixel) double-buffered offset screens. This is the effect I used for my scroll routine which gives single (MODE 0) pixel hardware scrolling. To achieve single pixel scrolling in MODE 1 would require 4 screens, each offset by 1 pixel. There are a couple of inherent problems using this method.
a) In order to display a sprite that moves relative to the background at a pixel accurate position requires either storing the sprite data twice (or four times for MODE 1), offset by 1 pixel, or using a relatively complex sprite rendering routine capable of drawing the sprite offset by 1 pixel. If you didn't do this, the sprite would move left and right by 1 pixel on each frame.
b) The CRTC wrap-around address can end up anywhere in the middle of the display. eg. the first byte of a sprite may be at address #C7FF, the next horizontal byte will be at address #C000. So the sprite rendering routine can't simply use a simple Z80 instruction like
INC L or
INC HL to move to the next byte across, it needs to use either a combinations of
INC HL:RES 3,H for even lines and
INC HL:RES 4,H:SET 3,H for some odd lines,
INC HL:SET 4,H:SET 3,H for others. There are other ways to get around this problem using AND's, OR's, tables etc. The best way to maintain speed may be to test if the overlap will happen before rendering the sprite and use fast routines if it doesn't.