guruthree / mac-se-video-converter

Convert video from a Mac Plus/SE/Classic to a VESA VGA signal using a Raspberry Pi Pico
77 stars 5 forks source link

Lisa to VGA #3

Open warmech opened 8 months ago

warmech commented 8 months ago

Hello!

I've been playing around for a bit with your code to interface with a Lisa and have run into some issues. Full disclosure - video timing is not my strong suit; I have some experience working with 240p analogue RGB as it pertains to arcade displays, but this is much faster than I'm used to dealing with (and even then, it's been years since I've worked with standard res RGB video).

The Lisa outputs a 720x364 image with the following properties (see attached image):

I've been working on trying to get this to display on an 800x600x60 display to no avail; the end goal would be to have this centered both vertically (not the hardest thing to do) and horizontally (much harder - still trying to think this one through). Presently, all I've managed to get on-screen is a flickering, smashed several rows at the top of the display. I've set the system clock to 160MHz (nice and even divisibility down to the 40MHz the VGA clock runs at and 20MHz the Lisa one runs at) and am dividing by eight for the Lisa's clock. I've also updated the MAX_LINES directive to the Lisa's 364, updated the LINEBUFFER_LEN_32 directive from 16 to 23 (to just go over the Lisa's 720 dots per line - 22.5 was the exact divisor), and updated the VGA timings in vga.h using the referenced timings in the Pico SDK, but I'm a bit stuck on how to proceed at the moment. Might you be able to offer some advice on where to start in next?

timing

Edit: Ah, I see something else that's going to need modifying. Your assembly in videoinput.pio - it appears you're skipping lines for the horizontal retrace period in the pixelskiploop loop; maybe it's just because it's late, but I'm a bit confused as to how the timing on this works. It looks like the H-retrace on a compact Mac is ~164 pixels, which appears to be the number of pixels this skips (nop[29]*31 = 155, that plus the nine additional pixels comes out to 164), but how does the timing of the pixel clock line up with the nop instruction time? That just seems like... really awesome coincidence?

guruthree commented 8 months ago

OK it sounds like there are quite a few things going on. When you say you have several smashed rows at the top of the display, is that an issue with reading the Lisa video or generating VGA signal? During development I decoupled the VGA output and had the Pico grab single frames then output the captured data over USB so that I could figure out exactly what was being read. I did indeed have a lot of smashed lines, things particularly often looked like a vertically squished picture that was at a weird angle.

image1

If dumping over USB to work on the capture's not feasible I'd recommend going straight for generating an 800x600@60 Hz SVGA resolution, modifying the VGA timings for vga_timing_800x600_60_default according to http://tinyvga.com/vga-timing/800x600@60Hz which gives timings for a 40 MHz pixel clock. The Lisa's 20 MHz pixel clock and 40 MHz for SVGA are really convenient, and I think I'd aim for a 200 MHz system clock, with PIO dividers of 2 and 5 respectively. Faster is better here as it gives a lot more flexibility for things like centering the video signal. It sounds like this is more or less what you've done. You can then check VGA is working correctly by generating some dummy data in draw_from_sebuffer, I played around with checkerboarding or alternating lines:

void draw_grid_pattern(scanvideo_scanline_buffer_t *buffer) {
    uint line_num = scanvideo_scanline_number(buffer->scanline_id);

    uint16_t *p = (uint16_t *) buffer->data;
    *p++ = COMPOSABLE_RAW_RUN;

    if (line_num & 1)
        *p++ = black;
    else
        *p++ = white;

    *p++ = vga_mode.width - 2;

    memcpy(p, gridline + (line_num & 1), sizeof(uint16_t)*511);
    p += 511;

    *p++ = black; // off the screen?

    buffer->data_used = ((uint32_t *) p) - buffer->data;
    assert(buffer->data_used < buffer->data_max);

    buffer->status = SCANLINE_OK;

}

For getting pixel capture working, I think MAX_LINES would need to be 364 and LINEBUFFER_LEN_32 800/4/8 = 25 to have enough data in buffer to draw the SVGA as a starting point. There will be some tweaking between the LINEBUFFER and BUFFER variables to dial it in I think. LINE_OFFSET would I think be 14 according to the signal diagram to account for the vertical sync signal/retrace - the signal data doesn't start until well after the sync ends. Since there's no front porch I think the code for that in videoinput.pio lines 70 to 80 can be commented out. The horizontal backporch (retrace) is handled by waits, but since the data starts immediately in the rising edge the 0 and 1 for waiting I think need to be swapped. The life offset also needs to be updated in videoinput.pio by changing line 49 to set x, 14.

The alignment of the pixel clock and nop instructions was a bit of trial and error. Part of the magic occurs during the gpio_callback interrupt in se.h, which restarts the PIO clock divider in time with vsync (line 64). The next part of it is that I set the PIO clock divider to 2, leaving the PIO clock running significantly faster than the pixel clock. I think what this does is let the PIO sample in the middle of pulses, rather than hoping the rising edge is sharp enough that a perfectly aligned signal gets the correct reading. (I don't have a scope fast enough to verify what I think is happening actually happens though, so I'm not 100% certain.)

I think the waiting for sync issues and the videoinput PIO clock divider are probably what's messing things up the most right now.

warmech commented 8 months ago

Dang - I'm really curious how you managed to snag Mac image data over USB! That sounds incredibly useful!

As for the squashed image, here's what it looks like.

monitor

Here's what I've got for the VGA settings when drawing from TinyVGA:

const scanvideo_timing_t vga_timing_800x600_60_default =
        {
        .clock_freq = 40000000,

                .h_active = 800,
                .v_active = 600,

                .h_front_porch = 4 * 10,  //40
                .h_pulse = 16 * 8,            //128
                .h_total = 132 * 8,           //1056
                .h_sync_polarity = 0,

                .v_front_porch = 1,
                .v_pulse = 4,
                .v_total = 628,
                .v_sync_polarity = 0,

                .enable_clock = 0,
                .clock_polarity = 0,

                .enable_den = 0
        };

const scanvideo_mode_t vga_mode_800x600_60 =
{
        .default_timing = &vga_timing_800x600_60_default,
        .pio_program = &video_24mhz_composable,
        .width = 800,
        .height = 600,
        .xscale = 1,
        .yscale = 1,
};

#define vga_mode vga_mode_800x600_60

I'll reset the clock to 200e6 and SE_CLOCK_DIV to 2, but I'm a bit confused as to where the VGA divider is. I'm assuming it's implicitly stated by virtue of the .clock_freq declaration in the timing const in vga.h, given your comment in mac.c:

// 188/37.6 = 5 for VGA

When I'm home again this evening I'll give the dummy data generator a go to test out whether signal generation on my end is good (prediction: it's not, lol).

MAX_LINES I had set to 364, but I've bumped the LINEBUFFER_LEN_32 up from 23 to 25 as per your recommendation; LINE_OFFEST has also beet adjusted to 14. I've gone ahead and commented 70-80 in the PIO program and flipped the HSYNC values. Let me know if anything looks amiss:

.program videoinput

.wrap_target

    wait 0 pin 2 [0] // wait for vsync

    set x, 14 // skip the first LINE_OFFSET lines of data, nothing there anyway
vblankloop:
    wait 1 pin 1 [0] // fall then
    wait 0 pin 1 [0] // rise makes a complete hsync
    jmp x-- vblankloop

    set x, 0
    set y, 0
    pull noblock // move a 32-bit word from the TX FIFO into the OSR
    out x, 16 // right-hand 16 bits is the number of lines (345)
    out y, 16 // left-hand 16 bits is the number of pixels across (22*4*8=704)
    mov osr, y // copy y back to OSR so we can re-use it

    wait 0 pin 1 // wait for hsync
    wait 1 pin 1

hsyncloop:
    wait 0 pin 1 // wait for hsync

pixelloop:
    nop [1] // off set a bit the read so the timings match
    in pins, 1 [2] // read pixel data in every 2 cycles?

    jmp y-- pixelloop
    jmp x-- hsyncloop

.wrap

I'm glad to know that the amount of trial and error here is somewhat generous - this is all fairly new territory for me and a lot of what I've been doing to test beyond the obvious is very much just that. Your logic about the clock divider and catching data mid-pulse makes sense to me. I'll recompile and try again this evening and let you know my findings.

PS - Before I started making changes, I compiled your original code and flashed it to my Pico. With my Lisa hooked up to it, it DID display an image, though an understandably horribly shrunk and garbled one due to timing misconfigurations. You could, however, make out shapes on the Lisa's boot menu; not terribly helpful overall, but I was surprised I could get anything out of it with the stock 1024x768 settings!

guruthree commented 8 months ago

USB output was pretty bare bones. I disabled the interrupt so that only one frame of data was captured, then using the default basic Pico USB serial output stuff looped through sebuffer printing out its contents to the USB serial, which I then read in using Matlab to create images.

Your squashed picture is pretty encouraging, especially if there wasn't too much flicker! I saw a lot of similar output during development and it was always down to the input PIO timing.

The VGA timing struct looks correct. The VGA clock divider is set by the VGA library in scanvideo.c, calculating it from the system clock and the VGA clock_freq. (How it does this is a bit convoluted and I'm not entirely sure it's not actually running at a divider of 5 and not 2.5, but what ever it's doing has worked in the code already so an apparent divider of 5 should be fine.)

I think the pio program might needs a couple more tweaks, which are mostly my fault due to not remember how it worked correctly this morning...

Ahead of the pixelloop: label, the mov y, osr needs to be there - this is what tells it how many pixels to read in for each horizontal line so. Definetely need that, my bad!

After the pixelloop: label, the cycle count before a loop currently works out I think 6 cycles, so that combined with the clock divider of 2 you get the total 12 divider between the system clock and the SE pixel clock. In your case with a total divider of 10 for the Lisa, I think in pins, 1 [2] needs to be in pins, 1 [1]. That should be then a 2 cycle delay on the nop [1], 1 cycle on in, 1 cycle in [1] and then another cycle on jmp for 5 total - provided I'm remembering how this works correctly.

The pio program is told how many pixels across to read in se.h line 77 through the pio_sm_put() call, which moves across the number of pixels << 16 | number of lines which are read into the y and x registers respectively. When I constructed the code I didn't decouple the buffer size from the number of pixels to read as 512 was both what I wanted to read and display size. I think then you'll need to swap the LINEBUFFER_LEN_8*8 - 1 to 720 - 1. This will have the annoying knock-on effect though of not wrapping around correctly to match the size of sebuffer. The missing 80 pixels of the line would need to be made up somewhere. Thinking about it further I suppose LINEBUFFER_LEN_32 doesn't have to be the equivalent of 800 pixels across if you're careful reading out of sebuffer when copying from it in vga.h, but does have to be a multiple of 32 for the DMA copy. If you set LINEBUFFER_LEN_32 to 23, then only 32*23 = 736 - 720 = 16 pixels are missing, and a loop in the pio code could be used to insert these. I think after the hsyncloop: label you could insert

sixteenloop:
    set y, 15
    in null, 1
    jmp y-- sixteenloop

but I would recommend testing everything without this last change first as it will I think be really fiddly debugging this in combination with the rest of the timings stuff. I'm not 100% certain the in null, 1 is correct but if it works it should shift a zero onto the input shift register, similar to how pixel data is shifted into the ISR later in the code. With this the picture should be slightly offset to the right (by 16 pixels). Without this I think you'll just see displayed a slanted picture, similar to what I put in my last post.

Again, really great that you could see something resembling reality. Those glimpses of output really gave me hope while putting this together - I think at one point I even would up with a giant mouse cursor, which gave me a good laugh!

warmech commented 8 months ago

Hello! Minor update - I have not had a chance to sit back down and take a look at this in several days, but I had a sudden and horrible realization earlier today as to why I was achieving absolutely zero forward progress on this. So, I actually have two Lisas, a Lisa 2/5 and a 2/5 that was upgraded to a MacXL. I've been working on this by way of my XL which (in case you were not aware of the difference between a 2/5 and XL) has a special width coil attached to the CRT yoke and a different video state machine than a standard Lisa; this was done to provide a more "Mac-shaped" display for users running MacWorks (the Lisa's Macintosh compatibility layer) that would look less distorted on the Lisa's CRT. The original Lisa uses the display timings and resolution we've talked about previously (720x364); the XL with it's screen mod, however, outputs 608x432 using timings I can't appear to find documented anywhere. I cannot believe none of this occurred to me until now, lol.

All that having been said, I plan on switching to my 2/5 later this week when I have a moment and restarting my efforts from there. I'll probably hook my scope up to the XL and see what the timings look like afterwards. Either way, my optimism has been slightly restored in light of all this!