1 /** 2 hot-reloading functionality 3 4 currently can reload any file based assets, including shaders, textures, etc. 5 */ 6 7 module re.util.hotreload; 8 9 import std.format; 10 11 import re.core; 12 import re.content; 13 import re.util.interop; 14 import re.gfx.raytypes; 15 static import raylib; 16 17 interface Reloadable(T) { 18 bool changed(); 19 T reload(); 20 } 21 22 class ReloadableFile(T) : Reloadable!T { 23 string[] source_files; 24 long[] file_mod_times; 25 26 this(string[] source_files) { 27 this.source_files = source_files; 28 } 29 30 protected long get_file_mod_time(string file) { 31 if (file == null) 32 return 0; 33 return raylib.GetFileModTime(file.c_str); 34 } 35 36 bool changed() { 37 // make sure we have mod times for each file 38 if (file_mod_times.length != source_files.length) { 39 file_mod_times = []; 40 // preallocate space 41 file_mod_times.length = source_files.length; 42 // now fetch the mod times 43 for (int i = 0; i < source_files.length; i++) { 44 file_mod_times[i] = get_file_mod_time(source_files[i]); 45 } 46 // this is first time we are updating 47 return true; 48 } 49 // we already have previous entries for mod times, so check if any have changed 50 // get all most recent mod times 51 for (int i = 0; i < source_files.length; i++) { 52 auto prev_mod_time = file_mod_times[i]; 53 long new_mod_time = get_file_mod_time(source_files[i]); 54 file_mod_times[i] = new_mod_time; 55 if (prev_mod_time != new_mod_time) { 56 // at least one file has changed 57 return true; 58 } 59 } 60 return false; 61 } 62 63 abstract T reload(); 64 } 65 66 class ReloadableShader : ReloadableFile!Shader { 67 private enum VS_INDEX = 0; 68 private enum FS_INDEX = 1; 69 70 this(string vs_path, string fs_path) { 71 if (vs_path) 72 vs_path = Core.content.get_path(vs_path); 73 if (fs_path) 74 fs_path = Core.content.get_path(fs_path); 75 super([vs_path, fs_path]); 76 } 77 78 override Shader reload() { 79 // load shader, bypassing cache 80 auto vs_path = source_files[VS_INDEX]; 81 auto fs_path = source_files[FS_INDEX]; 82 Core.log.info(format("reloading shader: (vs: %s, fs: %s)", vs_path, fs_path)); 83 return Core.content.load_shader(vs_path, fs_path, true); 84 } 85 } 86 87 @("hotreload-basic") 88 unittest { 89 class ReloadableBag : Reloadable!int { 90 int beans; 91 int external_bean_register; 92 93 this(int beans) { 94 this.beans = beans; 95 this.external_bean_register = this.beans; 96 } 97 98 bool changed() { 99 return beans != external_bean_register; 100 } 101 102 int reload() { 103 beans = external_bean_register; 104 return beans; 105 } 106 } 107 108 // create bag 109 auto bag = new ReloadableBag(10); 110 assert(bag.beans == 10); 111 // check that it's not changed 112 assert(!bag.changed()); 113 114 // change bag 115 bag.external_bean_register = 20; 116 assert(bag.changed()); 117 // reload bag 118 assert(bag.reload() == 20); 119 } 120 121 @("hotreload-file") 122 unittest { 123 bool fake_changed = false; 124 long init_fake_time = 0; 125 long new_fake_time = 10; 126 int old_beans = 10; 127 int new_beans = 20; 128 129 class ReloadableMockFileBag : ReloadableFile!int { 130 this(string mock_bean_file) { 131 super([mock_bean_file]); 132 } 133 134 override long get_file_mod_time(string file) { 135 return fake_changed ? new_fake_time : init_fake_time; 136 } 137 138 override int reload() { 139 return fake_changed ? new_beans : old_beans; 140 } 141 } 142 143 // create bag 144 auto bag = new ReloadableMockFileBag("mock_bean_file"); 145 // it should be changed the first time, because on loading modtime it triggers a changed event 146 assert(bag.changed()); 147 assert(bag.reload() == old_beans); 148 // now, this time it should not be changed 149 assert(!bag.changed()); 150 assert(bag.reload() == old_beans); 151 // now, change the modtime 152 fake_changed = true; 153 // it should be changed now 154 assert(bag.changed()); 155 assert(bag.reload() == new_beans); 156 // it shold not be changed now 157 assert(!bag.changed()); 158 assert(bag.reload() == new_beans); 159 }