The program ddspgame is provided as a sample 64DD program. The program calls disk functions while displaying to the screen. It displays errors resulting from the function calls and the corresponding error messages, using the 64DD internal ROM fonts. The program's content is based on the sample game spgame, which has been included with the OS for some time, with the addition of programs for 64DD.
The purpose of this sample is to provide the hdd.h module, an implementation of 64DD processing sequences, as a model for processing sequences. Using this as a foundation, application creators can build original 64DD processing sequences and thereby quickly become familiar with creating applications for DD.
This section is organized as follows.
10.1.1 | A summary of the procedure for installing the hdd.h module. |
10.1.2 | Terminology definitions. |
10.1.3 - 10.1.8 | A detailed description of the procedure of 10.1.1 using the sample program ddspgame. |
10.1.9 - 10.1.11 | Description of the internal processing of the hdd.h module. |
First, a brief summary of the source-creation process used with the hdd.h module.
Terminology explanations and detailed descriptions of each step in the process are given in subsequent sections.
Module
A group of functionally similar functions. The declarations for each module usually are collected in a single header file. Thus, for convenience, the module is referred to by the name of this header file. The following 7 modules are defined in this sample.
cont.h font.h hdd.h main.h |
herr.h hscr.h htry.h |
sysassigned.h contains constants and macro definitions and sp_*.h contains sprite data. (Note: In the NW version, sp_*.h is created during make execution.) They are not modules. Header files used for library functions, such as ultra64.h and leo.h, also are referred to as modules here.
Higher-Order Module
Some modules provide extern functions in a larger functional unit that includes all the functions of another module. These are called higher-order modules. In this sample, hdd.h is designed as a higher-order module of leo.h. There is no higher-order/lower-order relation between the other modules. This sample adopts the policy that combined use of functions provided by higher-order and lower-order modules is not desirable.
Disk Thread
A thread used for execution and error handling of library functions that access 64DD.
Wait, Executable, and Execution Thread States
When a thread executes osRecvMesg( ) in block mode, the thread is placed in the wait state if there are no messages queued and in the executable state if a message already has arrived. Of the threads in the executable state, execution privilege is then granted to that with the highest priority. The thread with execution privilege is said to be in the execution state. For more information, see the references for the functions sSendMesg( ) and osRecvMesg( ).
Posting Processing Instructions
The placement of messages in the queue used to receive disk thread command instructions. In the disk thread, osRecvMesg( ) is executed for this queue in block mode, so the thread is placed in the executable state after the instructions are posted.
Wait
Refers to placement of the thread in the wait state until the next retrace.
Message Command
Refers to the TMessage4dd structure, which is passed to the disk processing thread as the OSMesg argument to osSendMesg(). Internal functions that perform various types of processing are called in the disk processing thread according to the value of the command member of the TMessage4dd structure. For more information on the commands, see command.jp in the sample directory.
Synchronous Function
Refers here to a function whose processing the called thread waits for completion of.
The main change in ddspgame from spgame is the addition of a dedicated disk processing thread. The main thread uses a function called dd_exec( ) to post processing instructions to the disk thread. Dividing processing across threads enables disk processing and screen rendering to be performed in parallel.
The following describes the main modules used by the sample program.
hdd.h
This module executes the 64DD library functions defined in leo.h and includes the error-handling sequences used after execution of these functions. Because hdd.h is a higher-order module of leo.h, the usual 64DD library functions are not used when hdd.h is used.
The following 6 functions are the main functions used. Specific examples of their usage are described in the subsequent sections. The file funcs.jp in the sample directory also describes the functions provided by this module.
hscr.h
A module added to spgame that has a screen-rendering section. test_64dd( ) is the central component of this application, which uses hdd.h. It displays the messages posted to the disk thread and the results of those postings.
herr.h
An error message rendering module. scr_error( ) is called by test_64dd( ) for every frame.
font.h
Defines functions for using fonts in DDROM.
hdd.h (dd.c, hdd_internal.h, dd_core.c, dd_func.c, and dd_checksum.c) was designed as an independent module that can easily be installed without modification in existing N64 programs. However, there are rules for using hdd.h. In addition, special editing is required for components that depend on methods specific to the application, such as error message rendering. The following are guidelines for installing and using the hdd.h module in an application.
First, before performing disk processing, call dd_create( ) from the thread issuing the disk commands. This creates and initializes the disk thread.
dd_create();
After performing the necessary initialization process with functions such as LeoInitialize( ), wait for command messages from other threads.
Use dd_exec( ) to pass messages to the disk thread. In the following example, the disk ID is obtained. Execution of dd_exec( ) stores the disk ID in _id.
Tmessage4dd _ddmsg;
...
_ddmsg.command = LEOCMD_GETID;
_ddmsg.param.hi = (s32)&_id;
_ddmsg.param.lo = 0;
dd_exec( &_ddmsg );
The Tmessage4dd structure is a command message passed to the disk thread. It is usually defined as a static or global variable.
typedef struct{ s32 command; s32 locked; /* 0 means available */ struct { s32 hi; s32 lo; s32 rw; s32 offs; }param; OSMesgQueue *retQ; s32 retVal; } TMessage4dd;
The member retQ is preset as a pointer to a message queue used for notification that the disk thread has terminated. Following processing of the command, the member retVal is set by the disk thread to a value indicating the status of the results of processing.
When a command is posted to the disk thread from another thread, the disk thread begins processing after all threads with a higher priority enter a wait state (eg, a retrace wait). Once processing is finished, the disk thread returns to the message wait state. The thread that posted the command obtains the results of processing using dd_checkGate( ). In this sample, the rendering thread that posted the command has an even lower priority than the disk thread. Although disk command processing finishes quickly in this case, problems such as decreased processing performance arise in cases such as heavy screen-rendering processing. Decreasing the priority of the disk thread places greater priority on screen rendering than on the processing speed of disk commands. Whether to give priority to the rendering thread or disk thread must be determined according to whether the application gives priority to rendering or to disk processing.
dd_checkGate( ) is used as follows.
static TMessage4dd _ddmsg = {0}; static s32 _step=0; if( dd_checkGate(&_ddmsg) ) { switch(_step) { case 0: /* Issue command. */ _ddmsg.command = LEOCMD_GETID; _ddmsg.param.hi = (s32)&_id; _ddmsg.param.lo = 0; dd_exec( &_ddmsg ); ++_step; break; case 1: /* Check ID. */ if( _ddmsg.retVal ) { assert( !bcmp(_id.gameName,"GAME",4) ); ++_step; } break; case2: ... } }
In this example, a command is issued to obtain the disk ID and the ID is then checked. The Tmessage4dd structure issued by dd_exec(&_ddmsg) is received by dd_checkGate(&_ddmsg). dd_checkGate( ) returns true if the status allows _ddmsg to be used for the next command. This usually means a status indicating that processing of the last command sent by dd_exec() has finished.
Note that if dd_checkGate(&_ddmsg) is not true, a value is not assigned to _ddmsg and _id is not referenced. A value of false for dd_checkGate(&_ddmsg) means that the last command set to _ddmsg is still being processed by the disk thread. Because the disk thread processes one command at a time, it is meaningless to send the next command until processing of the previous command sent to the disk thread has finished (although commands are processed in order as long as the command queue does not overflow). The reason why is that until dd_checkGate( ) returns true, there is no guarantee that the value of retVal and other return values will be same once command processing has finished (values set by the disk processing thread).
dd_flush( ) should be called for each frame in threads that perform a retrace wait (usually the scheduler thread or rendering thread). This is necessary to use wait functions in the disk thread. If this is not implemented, the application will not operate properly.
The following describes waits in somewhat greater detail. In the disk thread, 64DD library functions may be continuously called during processing of a single command, such as when a disk-insertion check is performed. If the disk thread is not freed during this time (ie, osRecvMesg() is not called in block mode), the rendering thread cannot be executed while the command is being processed (ie, error messages cannot be displayed). The functions that free the disk thread are referred to here as wait functions.
To restore to executable status a thread that was placed in the wait state by a wait function, an event or message from another thread must be sent to the message queue specified by osRecvMesg( ) in the wait function. dd_flush() is the function that sends this resume message.
Freeing a thread is not a process that is limited to 64DD processing sequences. It is a measure that is essential for parallel processing of multiple threads.
The rendering thread, aside from performing the rendering required for the application, checks dd_getStatus( ) and renders error messages as necessary. The procedure for error message rendering is described in err.c.
Error Message Rendering Procedure
void _print(Gfx **gpp) { ... _mes = dd_getStatus()->errmes; if( dd_getStatus()->mode & ( DDSTAT_DEAD | DDSTAT_DIALOGUE) ) { /* Mandatory message (areas 1 and 2) */ switch( _mes & 0xFF) { case ERRMES__SET_CLOCK: ... break; case ERRMES__RAMPACK_NOT_CONNECTED: ... ... } /* Mandatory message (area 3 ) */ switch( _mes & 0xFF00 ) { case ERRMES__DISK_WO_NUKE: ... break; ... } } /* Message specific to the application (if one exists) */ else if( dd_getStatus()->mode & DDSTAT_KILLED ) { switch( _mes & 0xFF) { case ERRMES__DRIVE_NOT_FOUND: ... break; } } ... }
(For more information on areas 1, 2, and 3, see Section 10.2, Error-Handling Sequences, *Displaying Error Numbers and Messages.)
Depending on the status of the disk processing thread, a variety of information is stored in the TDDStatus structure obtained by dd_getStatus( ). The rendering thread checks the structure with each rendering update and renders error messages as necessary.
The TDDStatus structure is as follows.
typedef struct { s32 mode; /* GOOD,BUSY,CONTINUE,KILLED,DEAD,DIALOG */ s32 errno; /* The error value of the previously executed LEO function */ s32 errmes; /* For error message display */ s32 counter; /* Reset counter */ s32 reserved; u32 buffer_size; /* Length of the data read */ u32 position; /* Position (LBA) of the reading head */ u32 read_offset; /* Offset for starting the read from the buffer */ } TDDStatus;
When using err.c, the creator of the application must modify the rendering procedure to suit the rendering method of the application. However, the only portions that require modification are the rendering method and the method of obtaining controller input. It is seldom necessary to modify message content or branching for dd_getStatus( )->errmes. Particularly for the mandatory messages, eliminating display processing and changing message content should be avoided. With non-mandatory error messages (including those specific to the application), branching may be added when issuance of a particular alert or notification message is desired, but such changes should be made only in cases like these.
When data are read from the RAM area of the disk, the integrity of the data must be checked by the application (see 7.9, Write Interrupts).
An example of a data check used when reading from theRAM area is shown in dd_checksum.c. In this sample, a checksum is located in the last 4 bytes of the specified read area. dd_checksum.c should be modified to suit the data storage format of the application.
The following are supplemental descriptions of the internal processing of the hdd.h and hfont.h modules.
10.1.9 Supplement 1: The Disk Thread Error-Handling ProcedureThis section uses an example to describe the flow of the error handling performed in hdd.h for the 64DD library functions. In the example used, the application sets dd_exec() to issue a read command. The flow of processing is followed beginning from coreObjectOfdd(), which is declared in dd_core.c. All error handling in hdd.h is based on that described in 10.2, Error-Handling Sequences, so please also refer to that section.
First, the disk thread receives a LEOCMD_LBAREAD message from dd_exec( ) in another thread. Once the disk thread is in the executable state, it sets the address of the read buffer and calls dd_read( ). In doing so, it passes as arguments the starting position of the read (first argument) and the size of the read (second argument).
case LEOCMD_BYTEREAD: _setBuffer((u8 *)msg->param.rw); next_mode = dd_read(msg->param.hi , msg->param.lo , UNITMODE_BYTE); if( next_mode == DDSTAT_GOOD ) { msg->param.rw = (u32)_rwbuf + dd_stat.read_offset; _retval = dd_stat.buffer_size; } break;
UNITMODE_LBA means that both arguments are to be treated as LBAs. msg is a pointer to Tmessage4dd structure passed by dd_exec( ). param.hi is set to the starting position of the read, and param.lo is set to the read size.
The dd_read( ) function is as follows.
s32 dd_read(s32 start_adr , s32 size ,s16 flag_convert ) { s32 start_lba, lba_size, actual_byte, byte_size; s32 err_code = -1,ret=-1; LEOCapacity cap; if (flag_convert == UNITMODE_BYTE) { start_adr &= 0x7FFFFFFF; size &= 0x7FFFFFFF; LeoByteToLBA(0 , start_adr , &start_lba); LeoLBAToByte(0 , (u32)start_lba , &actual_byte); if ( actual_byte > start_adr ) { --start_lba; LeoLBAToByte(0 , (u32)start_lba , &actual_byte); } dd_stat.read_offset = start_adr - actual_byte; LeoByteToLBA(start_lba,(u32)size+dd_stat.read_offset, &lba_size); } else /* UNITMODE_LBA */ { start_lba = start_adr; lba_size = size; dd_stat.read_offset = 0; } LeoLBAToByte( start_lba , lba_size , &byte_size ); dd_stat.position = start_lba; dd_stat.buffer_size = byte_size; /* used by error 23 */ if( byte_size > _rwbuf_size ) { err_code = LEO_ERROR_BUFFER_OVERFLOW; LeoRecv0(err_code,TLeoReadWrite); } else
{ LeoReadWrite(&_cmdBlk, OS_READ , (u32)start_lba , _rwbuf, lba_size , &diskQ ); LeoRecv(err_code,TLeoReadWrite); } |
/* check sum */ if( !err_code ) { LeoReadCapacity( &cap , OS_WRITE ); err_code = ((u32)start_lba < cap.startLBA ) ? 0 : dd_checkSum(); dd_stat.position = start_lba + lba_size; } return dd_errSeqDiskSeek( err_code ); }
This function is used to read from the disk. Its arguments are the starting position of the read and the read size. The units of the read are specified in either blocks or bytes. The return value is a status code such as DDSTAT_GOOD or DDSTAT_CONTINUE. The status code is finally returned to the thread that posted the command as the value of retValue of the Tmessage4dd structure. The meaning of each status code is as follows.
DDSTAT_GOOD | -- Normal termination |
DDSTAT_CONTINUE | -- Processing in progress |
DDSTAT_DEAD | -- Abnormal termination |
DDSTAT_KILLED | -- Forced Termination |
The portion in yellow is the portion that actually performs the read processing. LeoReadWrite( ) reads from the 64DD disk and starts the transfer of data to the buffer. LeoRecv (a macro that contains osRecvMesg( )) then waits for a processing-termination message from 64DD. During this period, the thread is put into a wait state and processing of other threads is performed.
LeoRecv sets err_code to the error code returned by LeoReadWrite( ). Processing corresponding to this error code is then performed by dd_errSeqDiskSeek( ).
For example, consider the case in which the disk is removed at this point. The error LEO_ERROR_MEDIUM_NOT_PRESENT is passed to dd_errSeqDiskSeek( ) and, as part of this error, dd_errSeqDiskChange( ) is called to request reinsertion of the disk.
_rwbuf and _rwbuf_size are set by the execution of _setBuffer( ) preceding the
call to dd_read ( ).
s32 dd_errSeqDiskChange(void) { s32 ret = -1; while(1) { while ( ( ret = dd_checkInsert() ) & DDSTAT_CONTINUE ) dd_wait(); if( ret != DDSTAT_INSERTED ) break; if( dd_confirmDisk() ) { ret = DDSTAT_GOOD; break; } dd_openDialog( ERRMES__WRONG_DISK ); while ( ( ret = dd_checkEject() ) & DDSTAT_CONTINUE ) dd_wait(); if( ret != DDSTAT_GOOD ) break; } return ret; }
As with dd_read( ), the functions dd_checkInsert( ) and dd_checkEject( ) called here include error codes for LeoReadDiskID( ) and LeoSpdlMotor( ) and pass the results of processing using DDSTAT_GOOD, DDSTAT_CONTINUE, DDSTAT_DEAD, and DDSTAT_KILLED.
dd_errSeqDiskChange( ) checks whether the correct disk is inserted and, until a forced or abnormal termination, repeatedly executes disk-insertion waits, ID checks, and (if the ID is incorrect) ejection checks. For information on forced and abnormal terminations, see 10.2, Error-Handling Sequences. During this time, a messages appropriate for the disk processing being performed is displayed for the game player by the main thread. The function dd_wait( ), which is called for disk-insertion waits and ejection checks, is a wait function that performs retrace waits to prevent needless calls to 64DD library functions. If a disk ID check is successful, dd_errSeqDiskSeek( ) returns DDSTAT_CONTINUE and dd_read( ) is consequently re-executed.
Before passing err_code to dd_errSeqDiskSeek( ), dd_read( ) performs a check (checksum) for data corruption, which cannot be detected as an error code. If a checksum error occurs, the disk thread rewrites to that area. This process is further described in the following section.
There are two types of disk thread command interruptions. One is the case when processing of a command is brought to a temporary halt by DDSTAT_KILLED. The other is when the thread is put in a wait state part way through processing a command and is made to wait for an external resume instruction. The former occurs with a RTC read failure at initialization, and the latter occurs when a checksum error occurs in the dd_read( ) checksum mentioned above.
RTC Read Failure at Initialization (with cassette startup)
If a LEO_ERROR_REAL_TIME_CLOCK_FAILURE error occurs with LeoReadRTC( ) at initialization, the disk thread brings command processing to a temporary halt with a DDSTAT_KILLED state and waits for the LEOCMD_SETRTC instruction. During a DDSTAT_KILLED state, the disk thread will not process the command it was passed unless the command fulfills the conditions of reinstatement. Once the LEOCMD_SETRTC instruction arrives, the disk thread executes dd_setRTC( ). If this function returns DDSTAT_GOOD, the disk thread executes dd_init( ) again and finishes processing the command. If dd_setRTC( ) returns a value other than DDSTAT_GOOD, command processing ends with an abnormal termination.
Occurrence of a Checksum Error with dd_read( )
When dd_checkSum( ) returns false, the disk thread calls dd_restoreData( ). dd_freeze( ) is called at the start of dd_restoreData( ), and the disk thread is temporarily interrupted. During this time, the main thread renders the following message and waits for user input: "The previous data could not be completely saved. The data will be erased. Please press button A." Once button A is pressed, the main thread calls dd_thaw( ) and resumes the disk thread. In the main thread, a rewrite is executed in dd_restoreData( ). Because this processing is contained in err.c, use of dd_thaw() requires no particular consideration by the application creator. For more information on checksum errors during reads, see 10.2, Error-Handling Sequences.
To make this sample program easy to understand, the font data in DDROM that are used in error messages are gathered at the time of the first rendering and transferred by DMA to a buffer in RAM.
The long PI time of the 64DD library functions makes coexistence of audio and sprite display processing somewhat difficult to implement. Inability of the application to handle the DMA delay appropriately may, depending on the case, cause disordered rendering or introduce noise.
Consequently, appropriate exclusion control of disk processing and other threads using PIs is needed to basically preclude DMA during disk access. Specifically, measures can be taken such as securing a large portion of the DMA buffer and gathering and loading the data needed in a particular case into memory in advance. The 4 MB of expansion memory in 64DD is essentially provided for use as a DMA buffer area. (Of course, this is not compulsory.)