Intercepting WRCHV

bbc micro/electron/atom/risc os coding queries and routines
Post Reply
julie_m
Posts: 347
Joined: Wed Jul 24, 2019 9:53 pm
Location: Derby, UK
Contact:

Intercepting WRCHV

Post by julie_m »

I've written some text wrapping / capital-after-full-stop code as part of my AdveBuilder adventure engine, which currently sits as a wrapper around OSASCI, called mainly from my text uncompression subroutine but also where there is a need to print raw text.

However, I was wondering whether it might be better if I redirected OSWRCH to my code? That way, PRINT statements in BASIC would go through the text wrapping directly without having to CALL a machine code routine explicitly. It's all running in MODE 7 to minimise space usage anyway, so there won't be any control sequences to worry about interfering with. It sort of feels like the way Acorn would have expected you to do it.

I'm thinking I will need to have an initialisation routine to get the address of the real MOS routine (since I don't know what machine it is running on) which will be in WRCHV at &20E-&20F, save that address into bytes 2-3 of a JMP instruction, and then have my code call that instead of OSWRCH:

Code: Select all

.wrap_on
    LDA &20F \ high byte of WRCHV
    BPL no_init \ skip this if WRCHV is pointing to RAM
    STA orig_oswrch+2 \ third byte of JMP instruction
    LDA &20E \ low byte of WRCHV
    STA orig_oswrch+1 \ second byte of JMP instruction
.no_init
    LDA new_oswrch
    STA &20E
    LDA new_oswrch+1
    STA &20F
    RTS

.wrap_off
    LDA orig_oswrch+2 \ third byte of JMP instruction
    BPL woff0 \ skip this if it is still pointing to RAM
    STA &20F \ high byte of WRCHV
    LDA orig_oswrch+1 \ second byte of JMP instruction
    STA &20E \ low byte of WRCHV
.woff0
    RTS    

.orig_oswrch \ JSR or JMP here instead of &FFEE
    JMP orig_oswrch \ this will get overwritten by wrap_on

.new_oswrch
    \ do text wrappingy stuff
    JMP orig_oswrch
Is this reasonable? wrap_on checks to see if WRCHV is pointing to RAM or ROM and if the latter, copies it into the JMP instruction at orig_oswrch; in any case it then sets WRCHV to point to new_oswrch. wrap_off checks to see if orig_oswrch is jumping to RAM or ROM and if the latter, it sets OSWRCH to point to the destination of the JMP. This initially points to itself (which is deadly; I could hard-code in a starting value, but then it would still break, and possibly in even nastier ways, on all but one MOS version) but will never get called, as orig_oswrch only ever gets called from new_oswrch, and nothing will call new_oswrch until WRCHV has been updated by wrap_on and by which time, the address of the MOS routine will already have been saved in the JMP. If wrap_off is called before wrap_on, orig_oswrch will be pointing to RAM and WRCHV will not be altered.

Or is this a Really Bad Idea for some reason I have not thought of?
gfoot
Posts: 134
Joined: Tue Apr 14, 2020 9:05 pm
Contact:

Re: Intercepting WRCHV

Post by gfoot »

julie_m wrote:
Thu Sep 09, 2021 10:46 pm
However, I was wondering whether it might be better if I redirected OSWRCH to my code? That way, PRINT statements in BASIC would go through the text wrapping directly without having to CALL a machine code routine explicitly. It's all running in MODE 7 to minimise space usage anyway, so there won't be any control sequences to worry about interfering with. It sort of feels like the way Acorn would have expected you to do it.
I think that's reasonable. I've seen code that does that before, e.g. to generate double-height output in MODE 7, or in some Superior Software games to generate scaled-up text output in graphics modes.
I'm thinking I will need to have an initialisation routine to get the address of the real MOS routine (since I don't know what machine it is running on) which will be in WRCHV at &20E-&20F, save that address into bytes 2-3 of a JMP instruction, and then have my code call that instead of OSWRCH:
I think it's more normal to just copy the old value to two bytes of data, then call it indirectly via JMP(...) like the OS does, rather than updating the JMP instruction itself. Not a big deal though.
Is this reasonable? wrap_on checks to see if WRCHV is pointing to RAM or ROM
I wouldn't do that by explicitly checking for a ROM address - better to compare it against your own value, or record some other way whether you've currently intercepted it or not (e.g. whether your copy of the old vector is zero or a valid address).

Something to be aware of is that other things might also intercept WRCHV. But depending on context, maybe you can guarantee that they won't.
Or is this a Really Bad Idea for some reason I have not thought of?
My biggest concern I guess is how will you implement word wrapping one character at a time - will you let the characters display on the screen and then move them later if the word is too long? Or buffer them until you receive a breaking character or reach the end of the line? It may be easier to implement word wrapping if you let the wrapping routine see all the text up front. But also less flexible!
User avatar
sweh
Posts: 2564
Joined: Sat Mar 10, 2012 12:05 pm
Location: New York, New York
Contact:

Re: Intercepting WRCHV

Post by sweh »

I'm guessing you're doing the "is it already in RAM" test so you can run your code multiple times and not get stuck in a loop. In the general case this could break (eg if something else had already taken the OSWRCH vector then you wouldn't update your orig values and so the JMP would go... who knows where). Having a "have I claimed it" flag would be more general.... and would allow chaining of intercepts :-)

But this is probably being toooo paranoid. I never bothered with this test BITD; just BREAK then "OLD" to cause all vectors to reset to default before running the next test :-)

But this edge-case aside, what you're doing is sensible. If you were to do this in a ROM you can't, of course, update the orig value so I used IND1V (&230) as a replacement indirect vector, but self-modifying the absolute JMP address in RAM is perfectly good.
Rgds
Stephen
julie_m
Posts: 347
Joined: Wed Jul 24, 2019 9:53 pm
Location: Derby, UK
Contact:

Re: Intercepting WRCHV

Post by julie_m »

gfoot wrote:
Thu Sep 09, 2021 11:02 pm
I think it's more normal to just copy the old value to two bytes of data, then call it indirectly via JMP(...) like the OS does, rather than updating the JMP instruction itself. Not a big deal though.
An indirect JMP takes an extra two cycles to read the real destination address; and bearing in mind the number of times this routine is going to get called, I was wanting to shave off as many cycles as possible, is all. I can be sure the code is going to be run from RAM; but in any case, if I was using an indirect JMP() I'd have to put the address in RAM, and a straightforward, absolute JMP is only one extra byte compared to that. The uncompression stuff slows it down enough already without an additional millisecond per screen of text .....
gfoot wrote:
Thu Sep 09, 2021 11:02 pm
I wouldn't do that by explicitly checking for a ROM address - better to compare it against your own value, or record some other way whether you've currently intercepted it or not (e.g. whether your copy of the old vector is zero or a valid address).

Something to be aware of is that other things might also intercept WRCHV. But depending on context, maybe you can guarantee that they won't.
I was assuming there wouldn't be room for anything else to intercept WRCHV. I guess I could alter the wrap_on test to save any address but new_wrch, and the wrap_off test not to restore orig_wrch (or omit it altogether and be very careful).
gfoot wrote:
Thu Sep 09, 2021 11:02 pm
My biggest concern I guess is how will you implement word wrapping one character at a time - will you let the characters display on the screen and then move them later if the word is too long? Or buffer them until you receive a breaking character or reach the end of the line? It may be easier to implement word wrapping if you let the wrapping routine see all the text up front. But also less flexible!
It buffers the text, so nothing actually appears until it flushes on a space or newline -- so no good for interactive typing. All that said, it would not be masses of extra work to change it and make it print text straight to the screen as soon as it is received, not print anything on a buffer flush within the line, but backspace out any word that overflowed its line and re-print it from the buffer at the beginning of the next line. I'll have to test that to see if it's too annoying .....
julie_m
Posts: 347
Joined: Wed Jul 24, 2019 9:53 pm
Location: Derby, UK
Contact:

Re: Intercepting WRCHV

Post by julie_m »

sweh wrote:
Thu Sep 09, 2021 11:36 pm
I'm guessing you're doing the "is it already in RAM" test so you can run your code multiple times and not get stuck in a loop. In the general case this could break (eg if something else had already taken the OSWRCH vector then you wouldn't update your orig values and so the JMP would go... who knows where). Having a "have I claimed it" flag would be more general.... and would allow chaining of intercepts :-)
That was the general idea: I want it to play nicely with BASIC and I can't be sure what state anything is in. So I want to be sure I'm saving the real OSWRCH in the MOS ROM, and never restoring the vector so it points to the initial catch-groove that exists before the old value has been saved. As long as I make sure to turn wrapping off before calling OSWORD &00 to get the command from the user, all should be fine.

And if I test more strictly for my own routine, then as you say it will allow chaining of intercepts. If there's room in memory for anything else .....
julie_m
Posts: 347
Joined: Wed Jul 24, 2019 9:53 pm
Location: Derby, UK
Contact:

Re: Intercepting WRCHV

Post by julie_m »

And, I'm pleased to say, it worked second time! I just needed to get rid of a call from inside my buffer-flushing code to OSNEWL (since that goes through WRCHV), and make &0A flush the word buffer (since OSNEWL actually sends &0A then &0D). Now enabling it correctly saves the old contents of WRCHV if it points to anything other than its own code, and disabling it correctly checks the saved old WRCHV contents point to anywhere but &FFEE (which is hardcoded in at assembly time) before restoring WRCHV. I think this makes it about as robust as it can be; buffer flushing will even work properly if my replacement code is called directly without saving an old WRCHV.

Actual code snippet:

Code: Select all

.real_wrap_on
    LDX &20E            \ low byte of WRCHV
    LDY &20F            \ high byte of WRCHV
    CPX #newwrch MOD256
    BNE _won_1
    CPY #newwrch DIV256
    BEQ _won_no_save
._won_1
    STX orig_wrch+1     \ second byte of JMP instruction
    STY orig_wrch+2     \ third byte of JMP
._won_no_save
    LDX #newwrch MOD256
    LDY #newwrch DIV256
    JMP _wrap_save_wrchv
    
.real_wrap_off
    LDX orig_wrch+1
    LDY orig_wrch+2
    CPX #&EE
    BNE _wrap_save_wrchv
    CPY #&FF
    BEQ _wrap_no_save
._wrap_save_wrchv
    STX &20E
    STY &20F
._wrap_no_save
    RTS
Coeus
Posts: 2357
Joined: Mon Jul 25, 2016 12:05 pm
Contact:

Re: Intercepting WRCHV

Post by Coeus »

julie_m wrote:
Sun Sep 12, 2021 7:06 pm
...checks the saved old WRCHV contents point to anywhere but &FFEE...
But WRCHV will never point to &FFEE - &FFEE is the location of a JMP (WRCHV) instruction so if you ever point WRCHV to &FFEE you would get an infinite loop.

I don't think you need to test both cases. When installing your handler you only need to check if WRCHV already points to your new version. If it does, do nothing. If it does not, save the old WRCHV into your JMP instruction and set your version as the new WRCHV.

When removing your handler, check if WRCHV points to your new version. If it does then copy the address back from your JMP instruction to WRCHV. If it doesn't then do nothing.
julie_m
Posts: 347
Joined: Wed Jul 24, 2019 9:53 pm
Location: Derby, UK
Contact:

Re: Intercepting WRCHV

Post by julie_m »

Coeus wrote:
Sun Sep 12, 2021 9:46 pm
But WRCHV will never point to &FFEE - &FFEE is the location of a JMP (WRCHV) instruction so if you ever point WRCHV to &FFEE you would get an infinite loop.
I know WRCHV will never point to &FFEE. That address is hard-coded into a JMP instruction at orig_wrch, which subsequently gets overwritten by wrap_on. This provides a sensible fallback in case newwrch is ever called before wrap_on has been called, as well as indicating to wrap_off that the stored "old address" is invalid.
Coeus wrote:
Sun Sep 12, 2021 9:46 pm
I don't think you need to test both cases. When installing your handler you only need to check if WRCHV already points to your new version. If it does, do nothing. If it does not, save the old WRCHV into your JMP instruction and set your version as the new WRCHV.

When removing your handler, check if WRCHV points to your new version. If it does then copy the address back from your JMP instruction to WRCHV. If it doesn't then do nothing.
Yes, that's almost exactly what I am doing. We compare the address in WRCHV to the address of our routine to decide if or not we need to change it, and we make sure we actually have done that before trying to restore it (not quite the same test you mention, but it's equivalent since WRCHV and orig_wrch+1..2 get poked by the same piece of code). So it is safe to call wrap_on multiple times, or to call wrap_off without first having called wrap_on.
User avatar
jgharston
Posts: 4545
Joined: Thu Sep 24, 2009 12:22 pm
Location: Whitby/Sheffield
Contact:

Re: Intercepting WRCHV

Post by jgharston »

julie_m wrote:
Sun Sep 12, 2021 11:06 pm
Coeus wrote:
Sun Sep 12, 2021 9:46 pm
But WRCHV will never point to &FFEE - &FFEE is the location of a JMP (WRCHV) instruction so if you ever point WRCHV to &FFEE you would get an infinite loop.
I know WRCHV will never point to &FFEE. That address is hard-coded into a JMP instruction at orig_wrch, which subsequently gets overwritten by wrap_on. This provides a sensible fallback in case newwrch is ever called before wrap_on has been called, as well as indicating to wrap_off that the stored "old address" is invalid.
If you're going that way you may as well make orig_wrch be JP 0 as that's a lot easier to check:
LDA orig_wrch+1:ORA orig_wrch+2:BEQ not_yet_intercepted

If you want to know what the default vector contents are, the default vector table is pointed to by &FFB7/8. So you can do:

LDA &FFB7:STA zp+0
LDA &FFB8:STA zp+1
LDY #vector-&200
LDA (zp),Y:STA &200,Y
INY
LDA (zp),Y:STA &200,Y
to reset a vector to its default value, or:
LDA (zp),Y:CMP &200,Y:BNE not_default
INY
LDA (zp),Y:CMP &200,Y:BNE not_default
; vector pointing to default address

Code: Select all

$ bbcbasic
PDP11 BBC BASIC IV Version 0.36
(C) Copyright J.G.Harston 1989,2005-2020
>_
julie_m
Posts: 347
Joined: Wed Jul 24, 2019 9:53 pm
Location: Derby, UK
Contact:

Re: Intercepting WRCHV

Post by julie_m »

jgharston wrote:
Mon Sep 13, 2021 12:33 am
If you're going that way you may as well make orig_wrch be JP 0 as that's a lot easier to check:
LDA orig_wrch+1:ORA orig_wrch+2:BEQ not_yet_intercepted
If I used JMP 0 for ease of detection, then that would cause problems if newwrch were ever called before wrap_on had been called. Perhaps I am just being paranoid; but if I'm writing this code with the explicit intention for other people to use it, it feels like a discourtesy on my part not to make it proof against at least the most likely careless use cases.

Nice tip about the default vector table, though!
Post Reply

Return to “programming”