diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 9e60fe3..348ec5a 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -30,7 +30,8 @@
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f''Release creee: id={d.get\\(\"\"id\"\", \"\"ERREUR\"\"\\)} tag={d.get\\(\"\"tag_name\"\", \"\"?\"\"\\)}''\\); print\\(d.get\\(''message'',''''\\)\\) if ''message'' in d else None\")",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f'' OK: {d.get\\(\"\"name\"\",\"\"?\"\"\\)} \\({d.get\\(\"\"size\"\",0\\)//1024//1024}MB\\)''\\)\")",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f''OK: {d.get\\(\"\"name\"\",d.get\\(\"\"message\"\",\"\"?\"\"\\)\\)} size={d.get\\(\"\"size\"\",0\\)//1024//1024}MB''\\)\")",
- "Bash(python3 -c \":*)"
+ "Bash(python3 -c \":*)",
+ "mcp__ide__getDiagnostics"
]
}
}
diff --git a/src/main/QueueManager.js b/src/main/QueueManager.js
index ad30904..8ca74b8 100644
--- a/src/main/QueueManager.js
+++ b/src/main/QueueManager.js
@@ -1,13 +1,13 @@
const BlenderProcess = require("./BlenderProcess.js");
const path = require("path");
const fs = require("fs");
+const os = require("os");
const { Notification } = require("electron");
const STR_STATUS_IDLE = "idle";
const STR_STATUS_RUNNING = "running";
const STR_STATUS_PAUSED = "paused";
-const STR_PLACEHOLDER_CONTENT = "RENDERING";
-const NB_PLACEHOLDER_MAX_SIZE = 64;
+const NB_PLACEHOLDER_MAX_SIZE = 512;
class QueueManager {
constructor(obj_window) {
@@ -19,6 +19,10 @@ class QueueManager {
this.nb_last_render_ms = 0;
this.str_last_image_path = null;
this.obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true };
+ this.str_hostname = os.hostname();
+ this.nb_total_render_ms = 0;
+ this.nb_completed_renders = 0;
+ this.map_remote_avgs = {};
}
set_notification_config(obj_config) {
@@ -38,6 +42,9 @@ class QueueManager {
this.str_status = STR_STATUS_RUNNING;
this.nb_last_render_ms = 0;
this.str_last_image_path = null;
+ this.nb_total_render_ms = 0;
+ this.nb_completed_renders = 0;
+ this.map_remote_avgs = {};
this.str_overwrite_mode = obj_config.str_overwrite_mode || "overwrite";
this.list_collections = obj_config.list_collections || [];
this.obj_render_settings = obj_config.obj_render_settings || null;
@@ -194,6 +201,8 @@ class QueueManager {
return;
}
+ this._check_remote_completions();
+
// Batch skip : boucle iterative pour eviter un stack overflow recursif
let nb_skip_count = 0;
while (this.nb_current_index < this.list_queue.length && this.str_overwrite_mode === "skip") {
@@ -215,7 +224,18 @@ class QueueManager {
if (nb_size === 0) {
break;
}
- obj_check.str_status = "skipped";
+
+ if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
+ obj_check.str_status = "skipped";
+ try {
+ obj_check.str_done_date = fs.statSync(obj_check.str_expected_file).mtime.toISOString();
+ } catch (obj_date_err) {
+ // ignore
+ }
+ } else {
+ obj_check.str_status = "rendering_remote";
+ this._read_placeholder(obj_check);
+ }
this.nb_current_index++;
nb_skip_count++;
}
@@ -242,13 +262,26 @@ class QueueManager {
try {
if (fs.existsSync(obj_item.str_expected_file)) {
let nb_recheck_size = fs.statSync(obj_item.str_expected_file).size;
- if (nb_recheck_size > 0) {
+ if (nb_recheck_size > NB_PLACEHOLDER_MAX_SIZE) {
obj_item.str_status = "skipped";
+ try {
+ obj_item.str_done_date = fs.statSync(obj_item.str_expected_file).mtime.toISOString();
+ } catch (obj_d_err) {
+ // ignore
+ }
this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (existant)");
this.nb_current_index++;
this._send_progress();
this._process_next();
return;
+ } else if (nb_recheck_size > 0) {
+ obj_item.str_status = "rendering_remote";
+ this._read_placeholder(obj_item);
+ this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (en cours par " + (obj_item.str_remote_hostname || "?") + ")");
+ this.nb_current_index++;
+ this._send_progress();
+ this._process_next();
+ return;
}
}
} catch (obj_recheck_err) {
@@ -264,7 +297,7 @@ class QueueManager {
if (!fs.existsSync(str_dir)) {
fs.mkdirSync(str_dir, { recursive: true });
}
- fs.writeFileSync(obj_item.str_expected_file, STR_PLACEHOLDER_CONTENT);
+ fs.writeFileSync(obj_item.str_expected_file, this._get_placeholder_content());
} catch (obj_file_err) {
this._send_log("ERREUR creation placeholder : " + obj_file_err.message);
}
@@ -301,6 +334,9 @@ class QueueManager {
obj_item.str_status = "done";
this.nb_last_render_ms = Date.now() - nb_start;
+ this.nb_total_render_ms += this.nb_last_render_ms;
+ this.nb_completed_renders++;
+ obj_item.str_done_date = new Date().toISOString();
let str_image = obj_result.str_rendered_file || obj_item.str_expected_file;
this.str_last_image_path = str_image;
@@ -356,14 +392,54 @@ class QueueManager {
let list_skipped = [];
let list_stopped = [];
let list_skipped_paths = [];
+ let list_rendering_remote = [];
+ let list_item_results = [];
+
+ let nb_avg = this.nb_completed_renders > 0
+ ? Math.round(this.nb_total_render_ms / this.nb_completed_renders)
+ : 0;
+
for (let nb_i = 0; nb_i < this.list_queue.length; nb_i++) {
- if (this.list_queue[nb_i].str_status === "skipped") {
+ let obj_q = this.list_queue[nb_i];
+ if (obj_q.str_status === "skipped") {
list_skipped.push(nb_i);
- list_skipped_paths.push({ nb_index: nb_i, str_path: this.list_queue[nb_i].str_expected_file });
- } else if (this.list_queue[nb_i].str_status === "stopped") {
+ list_skipped_paths.push({ nb_index: nb_i, str_path: obj_q.str_expected_file });
+ list_item_results.push({
+ nb_index: nb_i,
+ str_type: "done",
+ str_date: obj_q.str_done_date || null,
+ str_resolution: obj_q.nb_resolution_x + "x" + obj_q.nb_resolution_y,
+ });
+ } else if (obj_q.str_status === "stopped") {
list_stopped.push(nb_i);
+ } else if (obj_q.str_status === "rendering_remote") {
+ list_rendering_remote.push(nb_i);
+ list_item_results.push({
+ nb_index: nb_i,
+ str_type: "rendering_remote",
+ str_remote_hostname: obj_q.str_remote_hostname || "?",
+ nb_remote_avg_ms: obj_q.nb_remote_avg_ms || 0,
+ });
+ } else if (obj_q.str_status === "done") {
+ list_item_results.push({
+ nb_index: nb_i,
+ str_type: "done",
+ str_date: obj_q.str_done_date || null,
+ str_resolution: obj_q.nb_resolution_x + "x" + obj_q.nb_resolution_y,
+ });
}
}
+
+ let list_machine_avgs = [];
+ if (nb_avg > 0) {
+ list_machine_avgs.push({ str_hostname: this.str_hostname, nb_avg_ms: nb_avg });
+ }
+ for (let str_h of Object.keys(this.map_remote_avgs)) {
+ if (this.map_remote_avgs[str_h] > 0) {
+ list_machine_avgs.push({ str_hostname: str_h, nb_avg_ms: this.map_remote_avgs[str_h] });
+ }
+ }
+
this._send_event("render-progress", {
nb_current: this.nb_current_index,
nb_total: this.list_queue.length,
@@ -375,9 +451,57 @@ class QueueManager {
list_skipped: list_skipped,
list_stopped: list_stopped,
list_skipped_paths: list_skipped_paths,
+ list_rendering_remote: list_rendering_remote,
+ list_item_results: list_item_results,
+ str_hostname: this.str_hostname,
+ nb_avg_render_ms: nb_avg,
+ list_machine_avgs: list_machine_avgs,
});
}
+ _get_placeholder_content() {
+ let nb_avg = this.nb_completed_renders > 0
+ ? Math.round(this.nb_total_render_ms / this.nb_completed_renders)
+ : 0;
+ return JSON.stringify({ str_hostname: this.str_hostname, nb_avg_ms: nb_avg });
+ }
+
+ _read_placeholder(obj_item) {
+ try {
+ let str_content = fs.readFileSync(obj_item.str_expected_file, "utf-8");
+ let obj_data = JSON.parse(str_content);
+ obj_item.str_remote_hostname = obj_data.str_hostname || "?";
+ obj_item.nb_remote_avg_ms = obj_data.nb_avg_ms || 0;
+ if (obj_data.str_hostname && obj_data.nb_avg_ms > 0) {
+ this.map_remote_avgs[obj_data.str_hostname] = obj_data.nb_avg_ms;
+ }
+ } catch (obj_parse_err) {
+ obj_item.str_remote_hostname = "?";
+ obj_item.nb_remote_avg_ms = 0;
+ }
+ }
+
+ _check_remote_completions() {
+ for (let obj_q of this.list_queue) {
+ if (obj_q.str_status !== "rendering_remote") {
+ continue;
+ }
+ try {
+ if (fs.existsSync(obj_q.str_expected_file)) {
+ let nb_size = fs.statSync(obj_q.str_expected_file).size;
+ if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
+ obj_q.str_status = "skipped";
+ obj_q.str_done_date = fs.statSync(obj_q.str_expected_file).mtime.toISOString();
+ }
+ } else {
+ obj_q.str_status = "pending";
+ }
+ } catch (obj_check_err) {
+ // ignore
+ }
+ }
+ }
+
_send_log(str_message) {
this._send_event("log", str_message);
}
diff --git a/src/renderer/scripts/ProgressBar.js b/src/renderer/scripts/ProgressBar.js
index f0b31f6..3a39b8b 100644
--- a/src/renderer/scripts/ProgressBar.js
+++ b/src/renderer/scripts/ProgressBar.js
@@ -40,7 +40,7 @@ const ProgressBar = {
let obj_frame_label = document.getElementById("label_current_frame");
obj_frame_label.textContent = nb_frame !== null && nb_frame !== undefined ? String(nb_frame) : "-";
- RenderQueue.update_progress(nb_current, obj_data.nb_last_render_ms || 0, obj_data.str_last_image_path || null, obj_data.list_skipped || [], obj_data.list_stopped || [], obj_data.list_skipped_paths || []);
+ RenderQueue.update_progress(obj_data);
},
reset: () => {
diff --git a/src/renderer/scripts/RenderQueue.js b/src/renderer/scripts/RenderQueue.js
index b3307b0..9508bbf 100644
--- a/src/renderer/scripts/RenderQueue.js
+++ b/src/renderer/scripts/RenderQueue.js
@@ -3,6 +3,9 @@ const RenderQueue = {
nb_total_render_ms: 0,
nb_completed_renders: 0,
nb_last_current: 0,
+ list_machine_avgs: [],
+ nb_local_avg_ms: 0,
+ str_hostname: "",
init: () => {
// Initialized on demand
@@ -13,6 +16,9 @@ const RenderQueue = {
RenderQueue.nb_total_render_ms = 0;
RenderQueue.nb_completed_renders = 0;
RenderQueue.nb_last_current = 0;
+ RenderQueue.list_machine_avgs = [];
+ RenderQueue.nb_local_avg_ms = 0;
+ RenderQueue.str_hostname = "";
let list_enabled = [];
for (let obj_cam of list_cameras) {
@@ -32,6 +38,7 @@ const RenderQueue = {
str_image_path: null,
obj_dom_el: null,
obj_dom_icon: null,
+ obj_dom_result: null,
str_dom_status: null,
is_click_bound: false,
});
@@ -60,6 +67,7 @@ const RenderQueue = {
str_image_path: null,
obj_dom_el: null,
obj_dom_icon: null,
+ obj_dom_result: null,
str_dom_status: null,
is_click_bound: false,
});
@@ -101,12 +109,20 @@ const RenderQueue = {
obj_frame.classList.add("text-light-emphasis");
obj_frame.textContent = "F" + obj_item.nb_frame;
+ let obj_result = document.createElement("small");
+ obj_result.classList.add("text-light-emphasis", "queue-item-result");
+ obj_result.style.minWidth = "140px";
+ obj_result.style.textAlign = "right";
+ obj_result.style.fontSize = "0.75em";
+
obj_el.appendChild(obj_icon);
obj_el.appendChild(obj_name);
obj_el.appendChild(obj_frame);
+ obj_el.appendChild(obj_result);
obj_item.obj_dom_el = obj_el;
obj_item.obj_dom_icon = obj_icon;
+ obj_item.obj_dom_result = obj_result;
obj_item.str_dom_status = "pending";
obj_fragment.appendChild(obj_el);
@@ -115,7 +131,21 @@ const RenderQueue = {
obj_container.appendChild(obj_fragment);
},
- update_progress: (nb_current, nb_last_render_ms, str_last_image_path, list_skipped, list_stopped, list_skipped_paths) => {
+ update_progress: (obj_data) => {
+ let nb_current = obj_data.nb_current || 0;
+ let nb_last_render_ms = obj_data.nb_last_render_ms || 0;
+ let str_last_image_path = obj_data.str_last_image_path || null;
+ let list_skipped = obj_data.list_skipped || [];
+ let list_stopped = obj_data.list_stopped || [];
+ let list_skipped_paths = obj_data.list_skipped_paths || [];
+ let list_rendering_remote = obj_data.list_rendering_remote || [];
+ let list_item_results = obj_data.list_item_results || [];
+ let list_machine_avgs = obj_data.list_machine_avgs || [];
+
+ RenderQueue.str_hostname = obj_data.str_hostname || "";
+ RenderQueue.nb_local_avg_ms = obj_data.nb_avg_render_ms || 0;
+ RenderQueue.list_machine_avgs = list_machine_avgs;
+
if (nb_current > RenderQueue.nb_last_current && nb_last_render_ms > 0) {
RenderQueue.nb_total_render_ms += nb_last_render_ms;
RenderQueue.nb_completed_renders++;
@@ -134,9 +164,17 @@ const RenderQueue = {
}
}
+ // Build result map for quick lookup
+ let map_results = {};
+ for (let obj_r of list_item_results) {
+ map_results[obj_r.nb_index] = obj_r;
+ }
+
for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) {
if (list_stopped && list_stopped.indexOf(nb_i) !== -1) {
RenderQueue.list_items[nb_i].str_status = "stopped";
+ } else if (list_rendering_remote && list_rendering_remote.indexOf(nb_i) !== -1) {
+ RenderQueue.list_items[nb_i].str_status = "rendering_remote";
} else if (list_skipped && list_skipped.indexOf(nb_i) !== -1) {
RenderQueue.list_items[nb_i].str_status = "skipped";
} else if (nb_i < nb_current) {
@@ -146,6 +184,11 @@ const RenderQueue = {
} else {
RenderQueue.list_items[nb_i].str_status = "pending";
}
+
+ // Store result info on item
+ if (map_results[nb_i]) {
+ RenderQueue.list_items[nb_i].obj_result = map_results[nb_i];
+ }
}
RenderQueue._update_time_display();
@@ -163,7 +206,9 @@ const RenderQueue = {
let is_needs_click = (obj_item.str_status === "done" || obj_item.str_status === "skipped")
&& obj_item.str_image_path && !obj_item.is_click_bound;
- if (obj_item.str_status === obj_item.str_dom_status && !is_needs_click) {
+ let is_status_changed = obj_item.str_status !== obj_item.str_dom_status;
+
+ if (!is_status_changed && !is_needs_click) {
if (obj_item.str_status === "rendering") {
obj_rendering_el = obj_item.obj_dom_el;
}
@@ -189,10 +234,18 @@ const RenderQueue = {
} else if (obj_item.str_status === "stopped") {
str_icon = "mdi-stop-circle";
str_color = "text-warning";
+ } else if (obj_item.str_status === "rendering_remote") {
+ str_icon = "mdi-desktop-classic";
+ str_color = "text-warning";
}
obj_item.obj_dom_icon.className = "mdi " + str_icon + " " + str_color;
+ // Update result column
+ if (obj_item.obj_dom_result) {
+ RenderQueue._update_result_cell(obj_item);
+ }
+
if (is_needs_click) {
obj_item.obj_dom_el.classList.add("queue-item-clickable");
let str_path = obj_item.str_image_path;
@@ -210,6 +263,45 @@ const RenderQueue = {
}
},
+ _update_result_cell: (obj_item) => {
+ let obj_r = obj_item.obj_result;
+ if (!obj_r) {
+ obj_item.obj_dom_result.textContent = "";
+ return;
+ }
+
+ if (obj_r.str_type === "done") {
+ let str_date = "";
+ if (obj_r.str_date) {
+ let obj_d = new Date(obj_r.str_date);
+ let str_day = String(obj_d.getDate()).padStart(2, "0");
+ let str_month = String(obj_d.getMonth() + 1).padStart(2, "0");
+ let str_hours = String(obj_d.getHours()).padStart(2, "0");
+ let str_minutes = String(obj_d.getMinutes()).padStart(2, "0");
+ str_date = str_day + "/" + str_month + " " + str_hours + ":" + str_minutes;
+ }
+ let str_res = obj_r.str_resolution || "";
+ obj_item.obj_dom_result.textContent = str_date + (str_date && str_res ? " | " : "") + str_res;
+ obj_item.obj_dom_result.className = "text-success queue-item-result";
+ obj_item.obj_dom_result.style.minWidth = "140px";
+ obj_item.obj_dom_result.style.textAlign = "right";
+ obj_item.obj_dom_result.style.fontSize = "0.75em";
+ } else if (obj_r.str_type === "rendering_remote") {
+ let str_host = obj_r.str_remote_hostname || "?";
+ let str_avg = "";
+ if (obj_r.nb_remote_avg_ms > 0) {
+ str_avg = " ~" + RenderQueue._format_duration(obj_r.nb_remote_avg_ms) + "/i";
+ }
+ obj_item.obj_dom_result.textContent = str_host + str_avg;
+ obj_item.obj_dom_result.className = "text-warning queue-item-result";
+ obj_item.obj_dom_result.style.minWidth = "140px";
+ obj_item.obj_dom_result.style.textAlign = "right";
+ obj_item.obj_dom_result.style.fontSize = "0.75em";
+ } else {
+ obj_item.obj_dom_result.textContent = "";
+ }
+ },
+
mark_existing: (list_existing) => {
for (let obj_existing of list_existing) {
if (obj_existing.nb_index < RenderQueue.list_items.length) {
@@ -226,22 +318,55 @@ const RenderQueue = {
return;
}
- if (RenderQueue.nb_completed_renders === 0) {
+ let nb_local_avg = RenderQueue.nb_local_avg_ms;
+ if (nb_local_avg === 0 && RenderQueue.nb_completed_renders > 0) {
+ nb_local_avg = Math.round(RenderQueue.nb_total_render_ms / RenderQueue.nb_completed_renders);
+ }
+
+ if (nb_local_avg === 0) {
obj_label.innerHTML = "";
return;
}
- let nb_avg_ms = RenderQueue.nb_total_render_ms / RenderQueue.nb_completed_renders;
let nb_remaining_count = 0;
for (let obj_item of RenderQueue.list_items) {
- if (obj_item.str_status !== "done" && obj_item.str_status !== "skipped") {
+ if (obj_item.str_status !== "done" && obj_item.str_status !== "skipped" && obj_item.str_status !== "rendering_remote") {
nb_remaining_count++;
}
}
- let nb_remaining_ms = nb_avg_ms * nb_remaining_count;
- obj_label.innerHTML = ''
- + RenderQueue._format_duration(nb_remaining_ms);
+ // Part 1 : temps moyen machine actuelle /i
+ let str_local_avg = RenderQueue._format_duration(nb_local_avg) + "/i";
+
+ // Part 2 : temps restant si machine seule
+ let nb_remaining_solo_ms = nb_local_avg * nb_remaining_count;
+ let str_remaining_solo = RenderQueue._format_duration(nb_remaining_solo_ms);
+
+ // Part 3 : temps restant multi-machines
+ let list_avgs = RenderQueue.list_machine_avgs;
+ let nb_machines_with_data = 0;
+ let nb_sum_avgs = 0;
+ for (let obj_m of list_avgs) {
+ if (obj_m.nb_avg_ms > 0) {
+ nb_sum_avgs += obj_m.nb_avg_ms;
+ nb_machines_with_data++;
+ }
+ }
+
+ let str_remaining_multi = "";
+ if (nb_machines_with_data > 1) {
+ let nb_avg_of_avgs = nb_sum_avgs / nb_machines_with_data;
+ let nb_remaining_multi_ms = (nb_avg_of_avgs * nb_remaining_count) / nb_machines_with_data;
+ str_remaining_multi = RenderQueue._format_duration(nb_remaining_multi_ms);
+ }
+
+ let str_display = ''
+ + str_local_avg + " | " + str_remaining_solo;
+ if (str_remaining_multi) {
+ str_display += " | " + str_remaining_multi;
+ }
+
+ obj_label.innerHTML = str_display;
},
_format_duration: (nb_ms) => {