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