Filesystem

From dreamcast.wiki
Revision as of 14:02, 28 June 2020 by Unknown user (talk) (→‎VMU)
Jump to navigation Jump to search

Author: Andress Barajas

Introduction

This guide will focus on KOS's Virtual Filesystem (VFS). It helps to understand KOS's filesystem structure before starting any project. KOS has several mount points:

Dir Read Write Description
/cd Contains files that exist on the CD that is currently in the Dreamcast.
/pc The directory that links back to your computer if your Dreamcast is somehow linked to your computer (i.e. serial cable, BBA/LA)
/vmu Contains all the VMUs that are currently plugged in.
/ram Contains files that have been attached to the ramdisk.
/rd Contains the contents of the romdisk that was statically linked with the executable.
/pty NA NA This directory implements a pseudo-terminal filesystem (like Linux's /dev/pty).
/sd This directory appears when you successfully mount the SD card to /sd.

Helper Functions

Before we go into more detail about each directory listed in the table above, let's get acquainted with the helper functions that will make your job easier when dealing with KOS's virtual filesystem.

ssize_t fs_copy(const char *src, const char *dst)

The fs_copy() function copies the file at src to dst on the filesystem and returns the size of the file. You must be able to write to the dst base directory. You would mainly use this to cache files from /cd to /ram.

ssize_t fs_load(const char *src, void **out_ptr)

The fs_load() function opens and reads a whole file into RAM. It returns the size of the data and out_ptr points to the data in RAM.

ssize_t fs_path_append(char *dst, const char *src, size_t len)

The fs_path_append() function acts mostly like the function strncat(), with a few slight differences. When appending a path, this funcion inserts a '/' character if one doesn't already exist between dst and src. It returns the length of the new string (including the NUL terminator). len is the size of the dst buffer. It is used to make sure we stay in bounds of the dst buffer when appending a path.

CD

This directory contains all the files that exist on the CD. You can have a max of 8 files in /cd open at one time.

One thing to keep in mind when constructing a self bootable CD is that the location of a file on the CD affects the reading speeds of that file. This is because the GDROM in the Dreamcast works in constant angular velocity (CAV) mode. So files that exist on the outer sections of the CD are read faster (12x @ ~2 MB/sec) than the inner sections of the CD. Add a dummy file named "0.0" to push data to edge of the disk for faster reads.

PC

This directory links back to your computer if your Dreamcast is somehow linked to your computer (i.e. serial cable, BBA/LA). This directory is available when using DC-Load/DC-Tool combo to upload software to the Sega Dreamcast. By default, /pc points to the root directory of your computer. Root being:

  • C:// on Windows
  • Macintosh HD on Mac

To change the directory /pc points to, you can use the -c option in DC-Tool like so:

dc-tool-ip -t <IP Address> -c /path/to/mount/pc -x game.elf

You must be a super user. For Windows, run as administrator. For Mac users, use the sudo command (sudo dc-tool-ip -t.. etc).

VMU

This directory contains folders representing VMUs with the letternumber naming convention. The letter stands for which port the controller that the VMU is plugged into (a,b,c, or d) and the number stands for which slot the VMU is in (1 or 2).

Port a Port b Port c Port d
Slot 1 a1 b1 c1 d1
Slot 2 a2 b2 c2 d2

Inside these folders are game saves, VMU games, etc. Creating/Saving files to the VMU require special file headers that are covered in another tutorial. However, you can easily create a backup of your VMU files using fs_copy() and saving it to /pc or /sd.

Ramdisk

You only have one ramdisk available, and it's mounted on /ram. This directory is where you can cache many single files from the CD for faster reading access later on. Remember that each file you attach eats up RAM so be sure to detach any files you are not using. You can have a max of 8 files in /ram open at one time.

There are two functions you can use to "attach" a file to the ramdisk, fs_copy() and fs_ramdisk_attach():

int fs_ramdisk_attach(const char* fn, void* obj, size_t size)

The fs_ramdisk_attach() function takes a block of memory and associates it with a file on the ramdisk.

To release or detach a file from the /ram directory you must use fs_ramdisk_detach():

int fs_ramdisk_detach(const char* fn, void** obj, size_t* size)

The fs_ramdisk_detach() function retrieves the block of memory associated with the file, removing it from the ramdisk. You are responsible for freeing obj when you are done with it. Remember to fclose() any file before you detach it.

Examples of each are shown in the code snippets below.

Attach/Detach

bool attach_file(char* filepath) {
    void* filedata = NULL;
    char* filename = basename(filepath);
    ssize_t filesize = fs_load(filepath, &filedata);

    if(filesize != -1) {
        fs_ramdisk_attach(filename, filedata, filesize);
        return true;
    }
    else
        return false;
}
bool attach_file_v2(char* filepath) {
    char new_path[256] = {0};
    char* filename = basename(filepath);
    fs_path_append(new_path, "/ram", 256);
    fs_path_append(new_path, filename, 256);
    ssize_t filesize = fs_copy(filepath, new_path);

    return filesize > 0;
}
void detach_file(char* filename) {
    void* filedata = NULL;
    ssize_t filesize = 0;

    fs_ramdisk_detach(filename, &filedata, &filesize);
    free(filedata);
}

Romdisk

Romdisk is a small read-only filesystem. You can think of a romdisk as a single file(or folder) that contains other files. Each romdisk should be organized to contain assets that pertain to a specific level/stage making it easier to know when to load/unload them. You can have a max of 16 files open across all mounted romdisks at one time.

You want to use a romdisk for two reasons:

1. Reading a single file into RAM (from CD) is faster than reading many files because they can be spread out on the CD. Since a romdisk is just a collection of files in a single file, they can be read in one go. You can also gzip the romdisk to make it faster to read from the CD and then ungzip it before you mount it to RAM. I only recommend gzipping your romdisk if it contains uncompressed data files like WAV files, KMG/DTEX(Dreamcast specific images), BMP, etc.

2. Mounting the romdisk into RAM is considered caching the file. Reading from RAM is faster than reading files from the CD.

Create

Creating a romdisk is easy and is usually done inside the makefile with the following command:

$(KOS_GENROMFS) -f <RomdiskFileName> -d <RomdiskFolderPath> -v

You will need to create a folder and fill it with assets for each romdisk you want to create. Then execute the above command for each romdisk specifying a different <RomdiskFileName> and <RomdiskFolderPath>. After creating a romdisk you have two options:

1. **Recommended** Manually Mount/Unmount Romdisk - This approach allows you to mount multiple romdisks at one time at different mountpoints using fs_romdisk_mount(). Each romdisk you mount eats up RAM so be sure to unmount any romdisks you are not using with fs_romdisk_unmount(). Remember to fclose() any files that are open that reference a file that exists in the romdisk you are unmounting.

int fs_romdisk_mount(const char* mountpoint, const uint8* img, int own_buffer)

The fs_romdisk_mount() function will mount a romdisk image that has been loaded into memory to the specified mountpoint. The mountpoint is the directory name you want to load the romdisks' files from and must be in the format "/directory_name" (e.g. /stage1). This directory is then accessible from the root directory where all other paths (/cd, /ram/, etc) are located. You should pass "1" to own_buffer so that when you unmount the romdisk, the memory you passed in gets freed.

int fs_romdisk_unmount(const char* mountpoint)

The fs_romdisk_unmount() function unmounts a romdisk image that has been previously mounted with fs_romdisk_mount(). If you passed "1" to the own_buffer parameter, then this function will also free the memory.

- OR -

2. Statically Link Romdisk - Makes the romdisk a part of your executable which then automatically gets mounted at /rd. You can only statically link a single romdisk to your executable. Statically linking a romdisk is discouraged because the romdisk will always stay in RAM which may not be what you want. This approach is mainly used by developers to quickly demo their small programs using DC-Load/DC-Tool without having to enable CDFS redirection.

To understand how to statically link a romdisk, lets look at a simple KOS makefile:

Makefile

TARGET = main.elf
OBJS = romdisk.o main.o

all: clean $(TARGET)

include $(KOS_BASE)/Makefile.rules

clean:
	-rm -f $(TARGET) $(OBJS)
	-rm -f romdisk.o romdisk.img

$(TARGET): $(OBJS) 
	kos-cc -o $(TARGET) $(OBJS) $(DATAOBJS) $(OBJEXTRA)

romdisk.img:
	$(KOS_GENROMFS) -f romdisk.img -d romdisk -v

romdisk.o: romdisk.img
	$(KOS_BASE)/utils/bin2o/bin2o romdisk.img romdisk romdisk.o 

run: 
	$(KOS_LOADER) $(TARGET)

dist:
	rm -f $(OBJS) romdisk.o romdisk.img
	$(KOS_STRIP) $(TARGET)

After a romdisk is created, it is converted to an object file using the following command:

$(KOS_BASE)/utils/bin2o/bin2o <RomdiskFileName> <RomdiskReferenceName> <RomdiskObjFileName>

and then the romdisk obj file is added to OBJS:

OBJS = <RomdiskObjFileName> main.o

Now that the romdisk has been statically linked, we have to reference it in your program using the same <RomdiskReferenceName> that was used in the bin2o command. Somewhere above your main function add this:

extern uint8 <RomdiskReferenceName>[];
KOS_INIT_ROMDISK(<RomdiskReferenceName>);

and thats it!

Mount

bool mount_romdisk(char* filepath, char* mountpoint) {
    void *buffer = NULL;
    ssize_t filesize = fs_load(filepath, &buffer);

    if(filesize != -1) {
        fs_romdisk_mount(mountpoint, buffer, 1);
        return true;
    }
    else
        return false;
}

SD card

If you own a SD card adapter that plugs into the serial port of the Sega Dreamcast, you have the option to mount it on /sd so you can read/write to it. mount_sd_fat() below does all the necessary initialization and sets you up to read/write to a FAT formatted SD card. The function should be called near the beginning of your program. If you do any writing to the SD card, make sure to fclose() all files on the SD card before calling unmount_sd_fat().

In order for you to use these functions you must link with the libkosfat library in your makefile:

kos-cc -o $(TARGET) $(OBJS) $(DATAOBJS) $(OBJEXTRA) -lkosfat

Mount/Unmount

bool mount_sd_fat() {
    kos_blockdev_t sd_dev;
    uint8 partition_type;

    if(sd_init()) {
        printf("Could not initialize the SD card. Please make sure that you "
               "have an SD card adapter plugged in and an SD card inserted.\n");
        return false;
    }

    if(fs_fat_init()) {
        printf("Could not initialize fs_fat!\n");
        return false;
    }
        
    if(sd_blockdev_for_partition(0, &sd_dev, &partition_type)) {
        printf("Could not find the first partition on the SD card!\n");
        return false;
    }

    if(fs_fat_mount("/sd", &sd_dev, FS_FAT_MOUNT_READWRITE)) {
        printf("Could not mount SD card as fatfs. Please make sure the card "
            "has been properly formatted.\n");
        return false;
    }

    return true;
}
void unmount_sd_fat() {
    fs_fat_unmount("/sd");
    fs_fat_shutdown();
    sd_shutdown();
}

Source Code

You can find source code that covers all this material on Github