From fe3061ff7aa081557ea35b3b405c9024d6db6aba Mon Sep 17 00:00:00 2001 From: Adrien Moulin Date: Thu, 7 Jul 2022 23:35:34 +0200 Subject: [PATCH] Add HDD timing simulation - realistic seeking and read/write speed - read-ahead cache - write cache - preset system for performance characteristics --- src/disk/hdd.c | 352 +++++++++++++++++++++++++++++++++++++++- src/include/86box/hdd.h | 78 +++++++++ 2 files changed, 428 insertions(+), 2 deletions(-) diff --git a/src/disk/hdd.c b/src/disk/hdd.c index c2d619062..a9c840701 100644 --- a/src/disk/hdd.c +++ b/src/disk/hdd.c @@ -17,15 +17,19 @@ * Copyright 2017-2019 Fred N. van Kempen. */ #include -#include +#include #include +#include +#include +#include #include #include <86box/86box.h> #include <86box/plat.h> #include <86box/ui.h> #include <86box/hdd.h> #include <86box/cdrom.h> - +#include <86box/video.h> +#include "cpu.h" hard_disk_t hdd[HDD_NUM]; @@ -150,3 +154,347 @@ hdd_is_valid(int c) return(1); } + +double +hdd_seek_get_time(hard_disk_t *hdd, uint32_t dst_addr, uint8_t operation, uint8_t continuous, double max_seek_time) +{ + hdd_zone_t *zone = NULL; + for (int i = 0; i < hdd->num_zones; i++) { + zone = &hdd->zones[i]; + if (zone->end_sector >= dst_addr) + break; + } + + uint32_t new_track = zone->start_track + ((dst_addr - zone->start_sector) / zone->sectors_per_track); + uint32_t new_cylinder = new_track / hdd->phy_heads; + uint32_t cylinder_diff = abs((int)hdd->cur_cylinder - (int)new_cylinder); + + bool sequential = dst_addr == hdd->cur_addr + 1; + continuous = continuous && sequential; + + double seek_time = 0.0; + if (continuous) { + if (new_track == hdd->cur_track) { + // Same track + seek_time = zone->sector_time_usec; + } else if (!cylinder_diff) { + // Same cylinder, sequential track + seek_time = hdd->head_switch_usec; + } else { + // Sequential cylinder + seek_time = hdd->cyl_switch_usec; + } + } else { + if (!cylinder_diff) { + if (operation != HDD_OP_SEEK) { + seek_time = hdd->avg_rotation_lat_usec; + } else { + //seek_time = hdd->cyl_switch_usec; + seek_time = 50.0; + } + } else { + seek_time = hdd->cyl_switch_usec + (hdd->full_stroke_usec * (double)cylinder_diff / (double)hdd->phy_cyl); + if (operation != HDD_OP_SEEK) { + seek_time += hdd->avg_rotation_lat_usec; + } + } + } + + if (!max_seek_time || seek_time <= max_seek_time) { + hdd->cur_addr = dst_addr; + hdd->cur_track = new_track; + hdd->cur_cylinder = new_cylinder; + } + + return seek_time; +} + +static void +hdd_readahead_update(hard_disk_t *hdd) +{ + hdd_cache_t *cache = &hdd->cache; + if (cache->ra_ongoing) { + hdd_cache_seg_t *segment = &cache->segments[cache->ra_segment]; + + uint64_t elapsed_cycles = tsc - cache->ra_start_time; + double elapsed_us = (double)elapsed_cycles / cpuclock * 1000000.0; + // Do not overwrite data not yet read by host + uint32_t max_read_ahead = (segment->host_addr + cache->segment_size) - segment->ra_addr; + + double seek_time = 0.0; + + for (uint32_t i = 0; i < max_read_ahead; i++) { + seek_time += hdd_seek_get_time(hdd, segment->ra_addr, HDD_OP_READ, 1, elapsed_us - seek_time); + if (seek_time > elapsed_us) + break; + + segment->ra_addr++; + } + + if (segment->ra_addr > segment->lba_addr + cache->segment_size) { + uint32_t space_needed = segment->ra_addr - (segment->lba_addr + cache->segment_size); + segment->lba_addr += space_needed; + } + } +} + +static double +hdd_writecache_flush(hard_disk_t *hdd) +{ + double seek_time = 0.0; + while (hdd->cache.write_pending) { + seek_time += hdd_seek_get_time(hdd, hdd->cache.write_addr, HDD_OP_WRITE, 1, 0); + hdd->cache.write_addr++; + hdd->cache.write_pending--; + } + + return seek_time; +} + +static void +hdd_writecache_update(hard_disk_t *hdd) +{ + if (hdd->cache.write_pending) { + uint64_t elapsed_cycles = tsc - hdd->cache.write_start_time; + double elapsed_us = (double)elapsed_cycles / cpuclock * 1000000.0; + double seek_time = 0.0; + + while (hdd->cache.write_pending) { + seek_time += hdd_seek_get_time(hdd, hdd->cache.write_addr, HDD_OP_WRITE, 1, elapsed_us - seek_time); + if (seek_time > elapsed_us) + break; + + hdd->cache.write_addr++; + hdd->cache.write_pending--; + } + } +} + +double +hdd_timing_write(hard_disk_t *hdd, uint32_t addr, uint32_t len) +{ + hdd_readahead_update(hdd); + hdd_writecache_update(hdd); + + hdd->cache.ra_ongoing = 0; + + double seek_time = 0.0; + + if (hdd->cache.write_pending && (addr != (hdd->cache.write_addr + hdd->cache.write_pending))) { + // New request is not sequential to existing cache, need to flush it + seek_time += hdd_writecache_flush(hdd); + } + + if (!hdd->cache.write_pending) { + // Cache is empty + hdd->cache.write_addr = addr; + } + + hdd->cache.write_pending += len; + if (hdd->cache.write_pending > hdd->cache.write_size) { + // If request is bigger than free cache, flush some data first + uint32_t flush_needed = hdd->cache.write_pending - hdd->cache.write_size; + for (uint32_t i = 0; i < flush_needed; i++) { + seek_time += hdd_seek_get_time(hdd, hdd->cache.write_addr, HDD_OP_WRITE, 1, 0); + hdd->cache.write_addr++; + } + } + + hdd->cache.write_start_time = tsc + (uint32_t)(seek_time * cpuclock / 1000000.0); + + return seek_time; +} + +double +hdd_timing_read(hard_disk_t *hdd, uint32_t addr, uint32_t len) +{ + hdd_readahead_update(hdd); + hdd_writecache_update(hdd); + + double seek_time = 0.0; + seek_time += hdd_writecache_flush(hdd); + + hdd_cache_t *cache = &hdd->cache; + hdd_cache_seg_t *active_seg = &cache->segments[0]; + + for (uint32_t i = 0; i < cache->num_segments; i++) { + hdd_cache_seg_t *segment = &cache->segments[i]; + if (!segment->valid) { + active_seg = segment; + continue; + } + + if (segment->lba_addr <= addr && (segment->lba_addr + cache->segment_size) >= addr) { + // Cache HIT + segment->host_addr = addr; + active_seg = segment; + if (addr + len > segment->ra_addr) { + uint32_t need_read = (addr + len) - segment->ra_addr; + for (uint32_t j = 0; j < need_read; j++) { + seek_time += hdd_seek_get_time(hdd, segment->ra_addr, HDD_OP_READ, 1, 0.0); + segment->ra_addr++; + } + } + if (addr + len > segment->lba_addr + cache->segment_size) { + // Need to erase some previously cached data + uint32_t space_needed = (addr + len) - (segment->lba_addr + cache->segment_size); + segment->lba_addr += space_needed; + } + hit = true; + goto update_lru; + } else { + if (segment->lru > active_seg->lru) { + active_seg = segment; + } + } + } + + // Cache MISS + active_seg->lba_addr = addr; + active_seg->valid = 1; + active_seg->host_addr = addr; + active_seg->ra_addr = addr; + + for (uint32_t i = 0; i < len; i++) { + seek_time += hdd_seek_get_time(hdd, active_seg->ra_addr, HDD_OP_READ, i != 0, 0.0); + active_seg->ra_addr++; + } + +update_lru: + for (uint32_t i = 0; i < cache->num_segments; i++) { + cache->segments[i].lru++; + } + + active_seg->lru = 0; + + cache->ra_ongoing = 1; + cache->ra_segment = active_seg->id; + cache->ra_start_time = tsc + (uint32_t)(seek_time * cpuclock / 1000000.0); + + return seek_time; +} + +static void +hdd_cache_init(hard_disk_t *hdd) +{ + hdd_cache_t *cache = &hdd->cache; + cache->ra_segment = 0; + cache->ra_ongoing = 0; + cache->ra_start_time = 0; + + for (uint32_t i = 0; i < cache->num_segments; i++) { + cache->segments[i].valid = 0; + cache->segments[i].lru = 0; + cache->segments[i].id = i; + cache->segments[i].ra_addr = 0; + cache->segments[i].host_addr = 0; + } +} + +static void +hdd_zones_init(hard_disk_t *hdd) +{ + uint32_t lba = 0; + uint32_t track = 0; + + double revolution_usec = 60.0 / (double)hdd->rpm * 1000000.0; + for (uint32_t i = 0; i < hdd->num_zones; i++) { + hdd_zone_t *zone = &hdd->zones[i]; + zone->start_sector = lba; + zone->start_track = track; + zone->sector_time_usec = revolution_usec / (double)zone->sectors_per_track; + uint32_t tracks = zone->cylinders * hdd->phy_heads; + lba += tracks * zone->sectors_per_track; + zone->end_sector = lba - 1; + track += tracks - 1; + } +} + +hdd_preset_t hdd_presets[] = { + { .target_year = 1989, .match_max_mbyte = 99, .zones = 1, .avg_spt = 35, .heads = 2, .rpm = 3500, .full_stroke_ms = 40, .track_seek_ms = 8, + .rcache_num_seg = 1, .rcache_seg_size = 16, .max_multiple = 8 }, + + { .target_year = 1992, .match_max_mbyte = 249, .zones = 1, .avg_spt = 45, .heads = 2, .rpm = 3500, .full_stroke_ms = 30, .track_seek_ms = 6, + .rcache_num_seg = 4, .rcache_seg_size = 16, .max_multiple = 8 }, + + { .target_year = 1994, .match_max_mbyte = 999, .zones = 8, .avg_spt = 80, .heads = 4, .rpm = 4500, .full_stroke_ms = 26, .track_seek_ms = 5, + .rcache_num_seg = 4, .rcache_seg_size = 32, .max_multiple = 16 }, + + { .target_year = 1996, .match_max_mbyte = 1999, .zones = 16, .avg_spt = 135, .heads = 4, .rpm = 5400, .full_stroke_ms = 24, .track_seek_ms = 3, + .rcache_num_seg = 4, .rcache_seg_size = 64, .max_multiple = 16 }, + + { .target_year = 1997, .match_max_mbyte = 4999, .zones = 16, .avg_spt = 185, .heads = 6, .rpm = 5400, .full_stroke_ms = 20, .track_seek_ms = 2.5, + .rcache_num_seg = 8, .rcache_seg_size = 64, .max_multiple = 32 }, + + { .target_year = 1998, .match_max_mbyte = 9999, .zones = 16, .avg_spt = 300, .heads = 8, .rpm = 5400, .full_stroke_ms = 20, .track_seek_ms = 2, + .rcache_num_seg = 8, .rcache_seg_size = 128, .max_multiple = 32 }, + + { .target_year = 2000, .match_max_mbyte = 99999, .zones = 16, .avg_spt = 350, .heads = 6, .rpm = 7200, .full_stroke_ms = 15, .track_seek_ms = 2, + .rcache_num_seg = 16, .rcache_seg_size = 128, .max_multiple = 32 }, +}; + +void +hdd_preset_apply(hard_disk_t *hdd, hdd_preset_t *preset) +{ + hdd->phy_heads = preset->heads; + hdd->rpm = preset->rpm; + + double revolution_usec = 60.0 / (double)hdd->rpm * 1000000.0; + hdd->avg_rotation_lat_usec = revolution_usec / 2; + hdd->full_stroke_usec = preset->full_stroke_ms * 1000; + hdd->head_switch_usec = preset->track_seek_ms * 1000; + hdd->cyl_switch_usec = preset->track_seek_ms * 1000; + + hdd->cache.num_segments = preset->rcache_num_seg; + hdd->cache.segment_size = preset->rcache_seg_size; + hdd->max_multiple_block = preset->max_multiple; + + hdd->cache.write_size = 64; + + hdd->num_zones = preset->zones; + + uint32_t disk_sectors = hdd->tracks * hdd->hpc * hdd->spt; + uint32_t sectors_per_surface = (uint32_t)ceil((double)disk_sectors / (double)hdd->phy_heads); + uint32_t cylinders = (uint32_t)ceil((double)sectors_per_surface / (double)preset->avg_spt); + hdd->phy_cyl = cylinders; + uint32_t cylinders_per_zone = cylinders / preset->zones; + + uint32_t total_sectors = 0; + for (uint32_t i = 0; i < preset->zones; i++) { + uint32_t spt; + double zone_percent = i * 100 / (double)preset->zones; + + if (i < preset->zones - 1) { + // Function for realistic zone sector density + double spt_percent = -0.00341684 * pow(zone_percent, 2) - 0.175811 * zone_percent + 118.48; + spt = (uint32_t)ceil((double)preset->avg_spt * spt_percent / 100); + } else { + spt = (uint32_t)ceil((double)(disk_sectors - total_sectors) / (double)(cylinders_per_zone*preset->heads)); + } + + uint32_t zone_sectors = spt * cylinders_per_zone * preset->heads; + total_sectors += zone_sectors; + + hdd->zones[i].cylinders = cylinders_per_zone; + hdd->zones[i].sectors_per_track = spt; + } + + hdd_zones_init(hdd); + hdd_cache_init(hdd); +} + +void +hdd_preset_auto(hard_disk_t *hdd) +{ + uint32_t disk_sectors = hdd->tracks * hdd->hpc * hdd->spt; + uint32_t disk_size_mb = disk_sectors * 512 / 1024 / 1024; + int i; + for (i = 0; i < (sizeof(hdd_presets) / sizeof(hdd_presets[0])); i++) { + if (hdd_presets[i].match_max_mbyte >= disk_size_mb) + break; + } + + hdd_preset_t *preset = &hdd_presets[i]; + + hdd_preset_apply(hdd, preset); +} \ No newline at end of file diff --git a/src/include/86box/hdd.h b/src/include/86box/hdd.h index 96afcdde0..2a2a16bfc 100644 --- a/src/include/86box/hdd.h +++ b/src/include/86box/hdd.h @@ -72,6 +72,62 @@ enum { }; #endif +enum { + HDD_OP_SEEK = 0, + HDD_OP_READ, + HDD_OP_WRITE +}; + +#define HDD_MAX_ZONES 16 +#define HDD_MAX_CACHE_SEG 16 + +typedef struct { + uint32_t match_max_mbyte; + uint32_t zones; + uint32_t avg_spt; + uint32_t heads; + uint32_t rpm; + uint32_t target_year; + uint32_t rcache_num_seg; + uint32_t rcache_seg_size; + uint32_t max_multiple; + double full_stroke_ms; + double track_seek_ms; +} hdd_preset_t; + +typedef struct { + uint32_t id; + uint32_t lba_addr; + uint32_t ra_addr; + uint32_t host_addr; + uint8_t lru; + uint8_t valid; +} hdd_cache_seg_t; + +typedef struct { + // Read cache + hdd_cache_seg_t segments[HDD_MAX_CACHE_SEG]; + uint32_t num_segments; + uint32_t segment_size; + uint32_t ra_segment; + uint8_t ra_ongoing; + uint64_t ra_start_time; + + // Write cache + uint32_t write_addr; + uint32_t write_pending; + uint32_t write_size; + uint64_t write_start_time; +} hdd_cache_t; + +typedef struct { + uint32_t cylinders; + uint32_t sectors_per_track; + double sector_time_usec; + uint32_t start_sector; + uint32_t end_sector; + uint32_t start_track; +} hdd_zone_t; /* Define the virtual Hard Disk. */ typedef struct { @@ -100,6 +156,23 @@ typedef struct { spt, hpc, /* Physical geometry parameters */ tracks; + + hdd_zone_t zones[HDD_MAX_ZONES]; + uint32_t num_zones; + hdd_cache_t cache; + uint32_t phy_cyl; + uint32_t phy_heads; + uint32_t rpm; + uint8_t max_multiple_block; + + uint32_t cur_cylinder; + uint32_t cur_track; + uint32_t cur_addr; + + double avg_rotation_lat_usec; + double full_stroke_usec; + double head_switch_usec; + double cyl_switch_usec; } hard_disk_t; @@ -131,5 +204,10 @@ extern int image_is_hdi(const char *s); extern int image_is_hdx(const char *s, int check_signature); extern int image_is_vhd(const char *s, int check_signature); +extern double hdd_timing_write(hard_disk_t *hdd, uint32_t addr, uint32_t len); +extern double hdd_timing_read(hard_disk_t *hdd, uint32_t addr, uint32_t len); +extern double hdd_seek_get_time(hard_disk_t *hdd, uint32_t dst_addr, uint8_t operation, uint8_t continuous, double max_seek_time); +extern void hdd_preset_apply(hard_disk_t *hdd, hdd_preset_t *preset); +extern void hdd_preset_auto(hard_disk_t *hdd); #endif /*EMU_HDD_H*/