Rick's b.log - 2021/10/31 |
|
It is the 21st of November 2024 You are 3.15.7.212, pleased to meet you! |
|
mailto:
blog -at- heyrick -dot- eu
I woke at got up at half seven to feed the cats, looked out the window and was "is it a full moon or something?". But no, it's just everything is now an hour later. So I'll probably be going to work again as the sun rises rather than in darkness, but on the other hand I'll be coming home as the sun goes down. A little bit of time to sit in the evening sun? Nope, probably too low and too chilly now, by the time I get home.
As I was feeding the cats, the morning was arriving to a heavy sky.
For today's lunch, I decided on doing something a little special, since it's an important day. I started with a Greene's Cheesecake. This was easy to make, mix melted butter and biscuit crumb for the base, and then mix milk and powder for the topping. Assemble, stick in the fridge.
While that was in the fridge setting, or whatever it does, I peeled and chipified some potatoes, and put them in the oven along with a Just Chicken Fray Bentos pie.
While they were cooking, I carved the pumpkin. A generic smilie face this time. For me, Samhain is a happy festival. All ghosts welcome, ring the bell and I'll put the kettle on...
I was wearing the T-shirt that I made on Wednesday. Thankfully pumpkins are pretty easy to draw, so I did this one in about fifteen minutes in Draw. It took almost as long to print it using photo settings, but since I subscribe to HP's Instant Ink I don't need to worry about ink consumption.
Lunch. Niiiiice.
I also put up some appropriate decorations.
Tonight? Already lined up. Doctor Who if I remember. Then Motherland: Fort Salem on Prime video. A few episodes. Then, depending on how I feel, maybe an episode or two of Inside Job on Netflix.
And no pumpkin pie, soup, or ice cream. I don't actually like the taste of pumpkin. I just like putting candles in them. And, from time to time, blowing them up. ☺
It is a platform game. Part homage to the sorts of games I grew up with, and part wanting to create my own. You play the part of Lucy, a teenage girl who unwisely volunteered herself to explore Mamie Fletcher's House, a rumoured haunted house.
With twenty two levels (and some devious challenges designed by Vince), different styles of level design (the one pictured is, obviously, brick), two types of ghost, tea to drink, keys to find, and spiders to avoid...
Mamie Fletcher's House is available for RISC OS using the Store app, at the reasonable price of £4.99.
You can see some more screenshots and infomation at https://heyrick.eu/mamie/, and there's a two level playable demo available from there or from Store.
On the technical side, the program is entirely original code and it is written in C. I'll write some articles about the game design in a few weeks (perhaps in December).
Annoyingly, and perhaps intentionally trying to be obtuse, the software is provided in Archive format (rather than Zip). This means that not only can it only be unpacked on RISC OS (and not looked at on a phone or PC), but you'll also get some gibberish file type handed to you (NetSurf defaults to XML) because Zip is a known standard, Arc is not.
Once it is open, you have !HelloWimp, which as promised is a 300 byte executable that loads and displays a template with a window that says "Hello Wimp!". There is also a licence file, and finally the important part, the source code. It is written in BASIC using the built-in assembler.
The first thing to note is that the !Run file sets up a default slot of 32K. One would say for this program, 4K would suffice, but asmusingly it crashes if we give it a slot of 4K even though it doesn't need anything like that much. We'll see why in just a moment.
Now let's turn our attention to the source code. If you wonder why I'm being really nit-picky here, this is being offered as a sort of tutorial on how to write a simply and small program in assembler. Therefore, it would help if it didn't do odd things and/or teach wrongness and bad habits.
Applications run at &8000, which is why he is using offset assembly here. That said, as long as references are kept relative (which is normal for branches and ADR both of which are PC-relative), it doesn't need to be built with offset assembly because it won't matter where it is actually run.
Ah, some cheating on the file size. A missing AIF header. This is not strictly necessary for RISC OS 5, however I understand RISC OS 4 may require it. Personally, I think it ought to be mandatory in order that non-32 bit software can be easily detected and rejected.
Note, also, the peculiar use of
What a lot of twaddle, and quite broken.
It appears as if he is wanting to save and restore the registers around the program? I'm not exactly sure. Should that
It's pointless. One should make no assumptions about the status of any of the registers on entry to an Absolute (&FF8). For what it is worth, this is what RISC OS did for a tiny dump program I just threw together:
The state of most of the registers are what FileSwitch may have used in order to start up the application. The only registers that are specifically defined are
So no registers need preserved, and nothing should be assumed. Doing so is weird, and a waste of time. One can save 16 bytes by cutting out that rubbish.
And now we get to the truth of why 4K crashes the application. David works out the code end, and then adds 8K.
If you don't need registers, don't waste time preserving them. It is also recommended that you code with the expectation of APCS behaviour (that is to say, a function can freely use
Additionally, the task handle and magic word are unnecessary. They are for closing a task that is not currently paged in (such as a module task). For a regular application, you only need call the SWI. It will be assumed that it's the current task that is closing.
Did you spot the bug, by the way? We're loading
Seriously, do not write stuff in assembler these days, just don't.
Initialising as a RISC OS 2.00 task (no message filtering). That's just lame.
Again, no registers are required to be preserved around the function, so APCS rules (save
Defensive coding, folks. Don't make assumptions.
A simple enough polling loop, that recycles the window block for the poll event data (this is okay, the Wimp copies the window data for its own use).
You can see the "is it CloseWindow" block does this, if we again ignore the errant setting of
The final bit looks for either sort of message (normal and recorded) for a message code of zero. That means quit the application. It works, but I'd have been inclined to put the
The embedded data. The first align is unnecessary, it's following code so it won't not be aligned.
Now for some additional awfulness. The variables end is 32 bytes beyond the end of the code. The task handle follows the code. The window handle is four bytes back from the end of the variables. And the twenty four bytes in between? The window block follows the end of the vairables, and as it does double duty as the poll block, it is 256 bytes in size. The indirected data area follows that.
It might have been better to follow
What I can say is "it appears to work", but it contains...
All in all, if I was teaching my daughter assembler (I wouldn't, there's no need these days) then I'd expect her to write code like this as a ten year old. I'd consider myself a failure of a father and a geek if she made it to be a teenager and wrote such rubbish.
Samhain (Halloween)
It's that time of the year. And the final Sunday in October is also the final day of October, so we all get to experience yet another failure of the EU to effect real change to the lives of ordinary people by doing away with the mostly-despised messing around with the clocks twice a year.
Not a good weather sky.
Making the cheesecake.
Chips and pie in the oven.
A carved pumpkin.
A T-shirt I made myself featuring a carved pumpkin with a mask.
Lunch.
On the right, I'm writing this in Zap.
You can also see the SNES style controller, used for testing/playing Mamie (see below), plus the satellite receiver's control gizmo. And some packs of tea biscuits because...tea.
Oh, and that flat ribbon on the right. That's where the other HDMI cable went. Right in bloody front of me. 🤦 🤦 🤦
Decorations.I wrote a game!
I started writing a game during my summer holiday, and the version I had towards the end of it was "okay if a bit basic". My faithful beta testers (David, Vince, and Tony) provided numerous suggestions and my summer holiday project got a lot longer, bigger, and most importantly better.
Screenshot, scaled down from HD
Armed with only a camera, and the local librarian's suggestion that taking photos of ghosts can get rid of them, you venture in. Because, come on, there's no such thing as a ghost. Right? Right?
It is aimed at the Pi2 as a baseline (that is to say, ~900MHz ARMv7 processor). It runs a bit slow on the original Pi. I understand it will work under emulation, if the host machine is a fast one. At any rate, the demo is a slightly feature-reduced version of the real thing, so if the demo works well, the full version will do likewise.
Debugging a tiny Wimp program
As part of his tutorials, DavidS has released a Tiny Wimp program, available from https://asmfun.riscos.fr/down.html.
DIM code% 1024
FOR pass%=4 TO 6 STEP 2
P% = &8000
O% = code%
[OPT pass%
;***** BEGIN AIF HEADER ****************************************
;ANDEQ R0,R0,R0 ;NOP, not compressed.
;ANDEQ R0,R0,R0 ;Not self relocating.
;ANDEQ R0,R0,R0 ;No zero init.
;BL Start% ;Our Entry point.
;SWI "OS_Exit" ;Safety exit in case of return.
;EQUD RoSize% ;Code section size (mult of 4)
;EQUD 0 ;No data outside of the code area.
;EQUD 0 ;No debug code.
;EQUD 0 ;No zero init.
;EQUD 0 ;No debuging.
;EQUD &8000 ;Image base address.
;EQUD 0 ;No relocation workspace.
;EQUD &20 ;We are 32-bit safe.
;EQUD 0 ;No seperate data section.
;EQUD 0 ;Reserved, should be 0.
;EQUD 0 ;Reserved, should be 0.
;ANDEQ R0,R0,R0 ;No debug init.
;***** END AIF HEADER ******************************************
ANDEQ R0, R0, R0
for a zero word. It's either a zero or a branch, so EQUD 0
would have sufficed here. It is also a broken AIF header, as it is supposed to be 128 bytes long, although as is usual for RISC OS, this isn't enforced.
Notice also the confusion between "Code" and "Data" areas, which in the AIF spec are given as Read-Only and Read/Write. This would be important if RISC OS paid attention to this, because he is marking the program as Read-Only, with no Read/Write, and then dumping registers to a stack set up... in... uh... the 'nothing' area.
.Start%
MOV R0,R13 ;Setup stack.
ADR R13,codeEnd%
ADD R13,R13,#8192
STMFD R13!,{R0,R14}
BL init% ;Call init procedure.
BL pollLp% ;Drop into programs Poll Loop.
BL closeOut% ;Clean up, prepare to quit.
LDMFD R13!,{R0,R14} ;Restore calling stack.
MOV R13,R0
MOV R0,#0 ;No error.
SWI "OS_Exit" ;Exit, quit out.
STMFD
instruction say R0-R14
(R0
to R14
and all in between) instead of R0,R14
(only R0
and R14
)?
R0 &00000001 R1 &00008000 R2 &00008000 R3 &000000A0 R4 &00008000 R5 &3010509C R6 &00000000 R7 &000000A0 R8 &00000001 R9 &30107DBC R10 &FA208000 R11 &FA207FA0 R12 &80000000 R13 &80000000 R14 &FC050C94 R12
and R13
which are intentionally set to a dud value (R12
because that was the Arthur stack), and R14
which is a safety return address for programs that don't call OS_Exit
like they should (it points to an OS_Exit
instruction).
The fact that a 4K slot causes a crash is why you shouldn't ever do it like this. Instead, you should call OS_GetEnv
, which will return the memory limit in R1
. You can either set R13
to that (because it's a fully descending stack, so will work downwards from the top), or do some maths to ensure that you have enough space.
Never just blindly add random large amounts and hope it's good enough. Of course, if you use a real language, it will take care of the allocations for you.
;*********************************
;PROCEDURE = closeOut *
;PARAMETERS = NONE *
;PURPOSE *
; Clean up in preperation for *
; quitting. *
;*********************************
.closeOut%
STMFD R13!,{R0-R1,R14}
LDR R0,tsk%
LDR R1,tmag%
SWI "Wimp_CloseDown"
LDMFD R13!,{R0-R1,R14}
R0-R3
and may return with those registers different than they were on entry).
In this case, there's no need to preserve R0
and R1
, and since we are running in user mode with no sub-functions, there's no point in stacking R14
either, a simple MOV PC, R14
at the end will return.
R14
off the stack into... R14
. Which means that we'll pass through a BLMI
to some random address and then pass right into the initialisation routine, before exiting through the OS_Exit
call.
It is bollocks like this that justifies my belief that, in 2021, nobody should be attempting to write anything in assembler. We invented compilers so we no longer have to worry about what is placed on the stack where, and where it gets pulled into.
Any argument about "oh, but compilers generate less efficient code" basically evaporates when you understand how easy it is to make fundamental errors when writing in assembler. It is, by its nature, completely unforgiving. More than once in patching stuff to work on my Pi2, I have come across instructions like LDR R10, [R10]!
(which means load R10
from the address pointed to by R10
, and then write back the base address to R10
- effectively a bogus instruction). A simple typo, just like the R14
above, but one that can cause weird and unwanted behaviour. Possibly the only reason this was never noticed is because the side effects go by so quickly they just aren't seen.
This isn't to say you can't write buggy code in high level languages, you can, but there's a compiler to catch a lot of mistakes and take care of the low level grunt work. Here, with assembler, the grunt work is on you. Always. And with zero tolerance for error.
;*********************************
;PROCEDURE = init% *
;PARAMETERS = NONE *
;PURPOSE *
; Set up program, in this case *
; we load our window template *
; create the window and open it.*
;*********************************
.tmag%
EQUD &4B534154 ;Magic number, in ASCII ="TASK".
.init%
STMFD R13!,{R0-R6,R14}
MOV R0,#200 ;
LDR R1,tmag%
ADR R2,TskName%
SWI "Wimp_Initialise"
STR R1,tsk%
ADR R1,template% ;Get win def from template file.
SWI "Wimp_OpenTemplate"
ADR R1,wb%
ADR R2,ind%
ADD R3,R2,#&100
MVN R4,#0
ADR R5,tmp%
MOV R6,#0
SWI "Wimp_LoadTemplate"
SWI "Wimp_CreateWindow" ;Register our window.
STR R0,winh%
STR R0,wb%
SWI "Wimp_CloseTemplate"
ADR R0,wb%
SWI "Wimp_GetWindowInfo"
SWI "Wimp_OpenWindow" ;Open the window.
LDMFD R13!,{R0-R6,R15}
R4-R6
), if anything, would do.
But the main problem that I can see here is the utter lack of error checking. Checking for errors isn't something that you retrofit at some nebulous "later", it has to be built into your mindset and come as a normal part of the process of writing software. I see all of these non-X form SWIs and no BVS and, frankly, this is just really bad code.
If you rename/remove the Templates file, you will indeed get a nice "File '<HelloWimp$Dir>.Template' not found" error, but it won't be your application doing that. Your application will be gone, control having been passed to the environment error handler.
Wimp_LoadTemplate
does not return an error if the window was not found. It sets R6
to zero. What will give an error is the fall-through to Wimp_CreateWindow
, which upon receiving bogus gibberish from the LoadTemplate SWI will more than likely report "There is not enough memory to create this window or menu".
;*********************************
;PROCEDRURE = pollLp% *
;PARAMETERS = NONE *
;PURPOSE *
; Main loop of program, getting *
; events by calling wimp poll.*
; This loop does not end until *
; the events say it is time to *
; quit the program. *
;*********************************
.pollLp%
STMFD R13!,{R0-R2,R14} ;
.p0lp%
ADR R1,wb%
MOV R0,#&1800 ;Poll flags in R0.
ORR R0,R0,#&31
SWI "Wimp_Poll" ;
CMP R0,#2 ;
MOVEQ R0,#0
SWIEQ "Wimp_OpenWindow" ;
BEQ p0lp%
CMP R0,#3 ;
MOVEQ R0,#0
BNE p0c0
SWI "Wimp_CloseWindow" ;
B p0dn%
.p0c0
CMP R0,#17 ;
CMPNE R0,#18
LDREQ R2,[R1,#16]
BNE p0lp%
CMPEQ R2,#0
BEQ p0dn%
B p0lp% ;
.p0dn%
LDMFD R13!,{R0-R2,R15} ;
The mask flags are set up by loading &1800 (don't return Gain/Lose caret) and then merging in &31 (don't return pointer entering/leaving window, or null events).
I can't help but feel that there would be a lot more clarity by pointing to a word in memory and loading it. The word being a binary bitmask (like EQUD %1100000110001
) so it is simpler to see what is and isn't being set. It would add one additional word to the code size.
Wimp_OpenWindow
doesn't use R0
on entry, so why set it to zero? There's also the assumption here that flags will be preserved across a SWI call. This was the case in the 26 bit world, but it's not a valid assumption in the 32 bit realm. It would be safer to follow the first comparison (is it OpenWindow?) with a BNE
to the next test.
R0
to zero.
BNE
following the second comparison, and then have the load and final comparison be unconditional. There's something that irks me about having tests out of order and/or far too many chained conditionals. This... I can't quite put my finger on it, but it feels wrong.
ALIGN
.tmp%
EQUS "hello"+STRING$(7,CHR$(0))
ALIGN
.template%
EQUS "<HelloWimp$Dir>.Template"+CHR$(0)
ALIGN
.TskName%
EQUS "Hello Wimp"+CHR$(0)
ALIGN
.codeEnd%
]
varEnd%=codeEnd%+32
tsk%=codeEnd%
winh%=varEnd%-4
wb%=varEnd%: ind%=wb%+256:
RoSize%=O%-code%
NEXT
Finally, and with some measure of stupidity, the entire space from the start of the program and including the variables is sized and given as the size of the read only block. It's a good thing there's no AIF header, I guess.
.codeEnd
with some EQUD statements for things like the task and window handles (only needs eight bytes, not thirty two!), and save to disc up until codeEnd
, as the addresses need to be known but the actual space doesn't need to be saved and/or reloaded. It would certainly be clearer than that mess.
Wimp_CloseDown
shall never return with the N flag set)
And, certainly, this shouldn't be used as an example of anything other than why nobody should write code in assembler these days...
Gavin Wraith, 31st October 2021, 17:40 Adding my peppercorn: 'preperation' should be spelled 'preparation'.Stuart Swales, 31st October 2021, 18:11 From current AIF document: NOP is encoded as MOV r0, r0.Stuart Swales, 31st October 2021, 18:19 MVN R4,#0 !!!
FFS Just write MOV R4,#-1 and have done with it. No brownie points for being a smartarse.Rick, 31st October 2021, 20:19 Sorry Stuart,
Bad immediate constant at line XXX.
😂Rick, 31st October 2021, 20:21 Fixed the format fail, too.
I wonder why it didn't show up in Netsurf? Hmm...druck, 31st October 2021, 21:01 You can use integer variables for assembler labels (% postfix), most people don't bother, but it would be good to be consistent, pick one and stick to it.Stuart Swales, 1st November 2021, 11:28 RE MVN/MOV Ah, that's probably BASIC being less helpful than AAsm/ObjAsm.Rick, 1st November 2021, 13:56 druck, I would imagine the missing '%' after "p0c0" is another typo, as the rest appear to be marked as integers. Or maybe it was added later and he forgot that all the rest have a '%' suffix?
But, yeah, consistency of urine...
© 2021 Rick Murray |
This web page is licenced for your personal, private, non-commercial use only. No automated processing by advertising systems is permitted. RIPA notice: No consent is given for interception of page transmission. |