Commit 1156b130 authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: D5c - Java overlap loop: condition scene N+1 under scene N's DP entry

Overlap-ring design ratified 07/17 (handoffs/2026-07-17_pose_d5_overlap_ring_design.md):
- TpJna/GpuQuadJna/GpuQuad: bindings for the D5a image-set ring
  (selectImageSet/execConditioningAsync/imageSetReadyRecord) and the D5b
  launch/collect split (execPoseSceneDpLaunch/Collect); base/JCuda =
  unsupported (serial order + blocking entry retained).
- CuasConditioning.conditionPreloadedSceneToGpuOverlap: same staged upload +
  same conditioning kernel into ring slot img_set on the copy stream (bytes
  identical by construction - the D5a slot-equivalence case); restores the
  previous set on any failure.
- CuasPoseRT: dpSceneEntry takes an overlap Runnable - launch -> condition
  the NEXT scene -> collect (the ~5.6 ms/scene conditioning+H2D stage hides
  under the ~12.5 ms chain); the D4 oracle replay stays blocking; the loop
  builds the runnable (preloaded harness only, next non-null scene), skips
  the serial conditioning for a pre-conditioned scene, and disables the
  overlap for the rest of the run on any failure (fail-safe; the previous
  set is restored, records unaffected).
- CuasRtParameters: saved checkbox pose_h2d_overlap (default ON, under
  pose_scene_dp) = the A/B knob.
Gates: mvn clean package + mvn test PASS; Stage0 40/40 (no new kernels);
native run_cases.sh ALL PASS (img_ring + scene_dp_split + all existing).
REAL-SCENE GATE PENDING: routine saves-OFF run, 497 Done lines byte-identical
to the 07/16 22:52 record, expect whole ~20.9 -> ~15.5 ms/scene, ~63-65
scenes/s (60 FPS PASS on the mean).
Co-authored-by: 's avatarClaude Opus 4.8 <claude-opus-4-8@anthropic.com>
Co-authored-by: 's avatarClaude Fable 5 <claude-fable-5@anthropic.com>
parent c673a448
......@@ -578,6 +578,62 @@ public class CuasConditioning {
// 3-B rung B0: one-shot conditioning-stage split (see above). By Claude on 07/16/2026.
private static boolean b0_condition_reported = false;
/**
* D5 overlap variant of the preloaded upload (design ratified 07/17/2026):
* condition ONE scene into device image-set ring slot img_set while the
* PREVIOUS scene's DP chain runs. All GPU-side work rides the copy stream
* (staged H2D + the async conditioning kernel + the set's ready event); the
* only host cost is the pinned staged writes. Bytes are identical to
* conditionPreloadedSceneToGpu by construction - same staged upload, same
* conditioning kernel, only the target buffer and the stream differ (the
* D5a slot-equivalence case). On ANY failure the previous set is restored
* as active and false is returned - the caller reverts to the serial order
* (fail-safe; nothing was consumed from the target set).
* By Claude on 07/17/2026.
*/
public static boolean conditionPreloadedSceneToGpuOverlap(
final QuadCLT scene,
final float [][] raw,
final float [][] cpu_scratch,
final Config cfg,
final int img_set, // target ring set (the NEXT scene's slot)
final int restore_set) { // active set to restore on failure
if ((scene == null) || (raw == null) || (raw.length == 0) || (raw[0] == null)) return false;
final Config use_cfg = (cfg != null) ? cfg : new Config();
if (use_cfg.rowcol_enable || (use_cfg.zero_dc && Double.isNaN(use_cfg.dc_level))) return false;
final int num_sens = raw.length;
final int num_pixels = raw[0].length;
for (int s = 0; s < num_sens; s++) {
if ((raw[s] == null) || (raw[s].length != num_pixels)) return false;
}
final GpuQuad gpuQuad = scene.getGPUQuad();
if (!gpuQuad.selectImageSet(img_set)) return false;
boolean ok = false;
try {
final boolean gpu_condition = prepareGpuConditioning(
gpuQuad, num_sens, num_pixels,
scene.getLwirScales(), scene.getLwirOffsets(),
scene.getLwirScales2(), scene.getFPN(), use_cfg);
final float [][] upload;
if (gpu_condition) {
upload = raw;
} else {
conditionLinearToFloat(raw, cpu_scratch, scene.getLwirScales(), scene.getLwirOffsets(),
scene.getLwirScales2(), scene.getFPN(), use_cfg);
upload = cpu_scratch;
}
scene.saveQuadClt();
scene.setHasNewImageData(false);
gpuQuad.setBayerImages(upload, true); // staged -> the copy stream; a legacy fallback stays correct, just serial
ok = gpu_condition ? gpuQuad.execConditioningAsync() : gpuQuad.imageSetReadyRecord();
} finally {
if (!ok) {
gpuQuad.selectImageSet(restore_set); // never leave a half-filled set active
}
}
return ok;
}
/** CPU float-output oracle for the immutable preloaded input. */
private static void conditionLinearToFloat(
final float [][] raw,
......
......@@ -3134,6 +3134,73 @@ public class GpuQuad{ // quad camera description
public String poseLastError() {
return "no native backend";
}
// ---- D5 overlap (design ratified 07/17/2026): device image-set ring + the
// launch/collect split of the scene entry. Base/JCuda: unsupported - the serial
// conditioning order and the blocking entry remain the path. By Claude on 07/17/2026. ----
public boolean supportsPoseSceneDpOverlap() {
return false;
}
/** Select (lazily allocating) device image set 'set' as the ACTIVE set - ALL image
* consumers and uploads follow it. Flip only AFTER launching the chain that consumes
* the previous set (descriptor fills snapshot the pointer array). */
public boolean selectImageSet(int set) {
return false;
}
/** Async in-place conditioning of the ACTIVE image set on the copy stream; records the
* set's ready event after the kernel (ONE event = uploaded+conditioned). */
public boolean execConditioningAsync() {
return false;
}
/** Record the active set's ready event on the copy stream (the CPU-conditioned
* fallback, where no async kernel runs - uploads only). */
public boolean imageSetReadyRecord() {
return false;
}
/** D5b launch half of execPoseSceneDp: same inputs, no readbacks - enqueue the whole
* per-scene chain and return immediately (want_trace/want_mstats fix what the later
* collect may read). Returns 1 (launched) or a negative. */
public int execPoseSceneDpLaunch(
IntersceneLmaFloat.Camera reference,
IntersceneLmaFloat.Camera scene,
float [] reference_xyz,
float [] reference_atr,
float [] scene_xyz,
float [] scene_atr,
int [] task_map,
float [] task_centers,
int num_cycles,
float lambda,
float pure_weight,
float rms_diff,
int num_tiles,
int corr_slot,
int ntile_shift,
int num_components,
boolean use_eigen,
float eig_min_sqrt,
float eig_max_sqrt,
float min_confidence,
boolean same_weights,
boolean prepare_light,
float [] centers,
float [] pull_targets,
float [] param_regweights,
float [] vector,
boolean want_trace,
boolean want_mstats) {
return -1;
}
/** D5b collect half: wait for the launched chain and fetch the end-of-scene results;
* returns the last cycle's corr-row count (>0) exactly like the blocking entry. */
public int execPoseSceneDpCollect(
float [] trace,
int [] mstats,
float [] result,
float [] vector_out,
float [] prep_out,
int [] stats) {
return -1;
}
public void execRBGA(
double [] color_weights,
......
......@@ -950,6 +950,123 @@ public class GpuQuadJna extends GpuQuad {
// entry, mid-run DP failure must degrade, not abort). By Claude on 07/17/2026.
return n;
}
// ---- D5 overlap (design ratified 2026-07-17): image-set ring + launch/collect split. By Claude on 07/17/2026. ----
@Override public boolean supportsPoseSceneDpOverlap() { return !rectilinear; }
@Override public boolean selectImageSet(int set) {
try {
final int rc = lib.tp_proc_img_set_select(proc, set);
if (rc != 0) System.out.println("GpuQuadJna.selectImageSet("+set+"): rc="+rc+" ("+lib.tp_last_error()+")");
return rc == 0;
} catch (UnsatisfiedLinkError e) {
System.out.println("GpuQuadJna.selectImageSet(): native missing ("+e.getMessage()+")");
return false;
}
}
@Override public boolean execConditioningAsync() {
try {
final int rc = lib.tp_proc_exec_conditioning_async(proc);
if (rc != 0) System.out.println("GpuQuadJna.execConditioningAsync(): rc="+rc+" ("+lib.tp_last_error()+")");
return rc == 0;
} catch (UnsatisfiedLinkError e) {
System.out.println("GpuQuadJna.execConditioningAsync(): native missing ("+e.getMessage()+")");
return false;
}
}
@Override public boolean imageSetReadyRecord() {
try {
final int rc = lib.tp_proc_img_set_ready_record(proc);
if (rc != 0) System.out.println("GpuQuadJna.imageSetReadyRecord(): rc="+rc+" ("+lib.tp_last_error()+")");
return rc == 0;
} catch (UnsatisfiedLinkError e) {
System.out.println("GpuQuadJna.imageSetReadyRecord(): native missing ("+e.getMessage()+")");
return false;
}
}
@Override public int execPoseSceneDpLaunch(
final IntersceneLmaFloat.Camera reference,
final IntersceneLmaFloat.Camera scene,
final float [] reference_xyz,
final float [] reference_atr,
final float [] scene_xyz,
final float [] scene_atr,
final int [] task_map,
final float [] task_centers,
final int num_cycles,
final float lambda,
final float pure_weight,
final float rms_diff,
final int num_tiles,
final int corr_slot,
final int ntile_shift,
final int num_components,
final boolean use_eigen,
final float eig_min_sqrt,
final float eig_max_sqrt,
final float min_confidence,
final boolean same_weights,
final boolean prepare_light,
final float [] centers,
final float [] pull_targets,
final float [] param_regweights,
final float [] vector,
final boolean want_trace,
final boolean want_mstats) {
final boolean have_pose = (scene_xyz != null) || (scene_atr != null) ||
(reference_xyz != null) || (reference_atr != null);
float[] poseVectors = null;
if (have_pose) {
poseVectors = new float[12];
copyPoseVector3(reference_xyz, poseVectors, 0);
copyPoseVector3(reference_atr, poseVectors, 3);
copyPoseVector3(scene_xyz, poseVectors, 6);
copyPoseVector3(scene_atr, poseVectors, 9);
}
try {
// negative return = the same FAIL-safe hook as execPoseSceneDp (no throw)
return lib.tp_proc_exec_pose_scene_dp_launch(
proc,
(reference != null) ? poseCameraMeta(reference) : null,
(reference != null) ? reference.radial : null,
(reference != null) ? reference.rByRDist : null,
(reference != null) ? reference.rByRDist.length : 0,
(reference != null) ? reference.ers : null,
(reference != null) ? reference.ers.length : 0,
(scene != null) ? poseCameraMeta(scene) : null,
(scene != null) ? scene.radial : null,
(scene != null) ? scene.rByRDist : null,
(scene != null) ? scene.rByRDist.length : 0,
(scene != null) ? scene.ers : null,
(scene != null) ? scene.ers.length : 0,
poseVectors, task_map, task_centers,
num_cycles, lambda, pure_weight, rms_diff,
num_tiles, corr_slot, ntile_shift,
num_components, use_eigen ? 1 : 0,
eig_min_sqrt, eig_max_sqrt,
min_confidence, same_weights ? 1 : 0,
prepare_light ? 1 : 0,
centers, pull_targets, param_regweights,
vector,
want_trace ? 1 : 0, want_mstats ? 1 : 0);
} catch (UnsatisfiedLinkError e) {
System.out.println("GpuQuadJna.execPoseSceneDpLaunch(): native missing ("+e.getMessage()+")");
return -100;
}
}
@Override public int execPoseSceneDpCollect(
final float [] trace,
final int [] mstats,
final float [] result,
final float [] vector_out,
final float [] prep_out,
final int [] stats) {
try {
return lib.tp_proc_exec_pose_scene_dp_collect(proc, trace, mstats, result,
vector_out, prep_out, stats);
} catch (UnsatisfiedLinkError e) {
System.out.println("GpuQuadJna.execPoseSceneDpCollect(): native missing ("+e.getMessage()+")");
return -100;
}
}
@Override public String poseLastError() { return lib.tp_last_error(); }
@Override public void uploadPoseCycleGeometry() {
setGeometryCorrection(); // gc (resets rbr_ready -> one in-chain rebuild, same bits)
......
......@@ -271,6 +271,44 @@ public interface TpJna extends Library {
float[] vector,
float[] trace, int[] mstats, float[] result,
float[] vectorOut, float[] prepOut, int[] stats);
// ---- D5 overlap (design ratified 2026-07-17): device image-set ring (D5a) +
// launch/collect split of the scene entry (D5b). By Claude on 07/17/2026. ----
/** Select (lazily allocating) device image set 'set' as the ACTIVE set - all image
* consumers and staged uploads follow it. Flip only AFTER launching the chain that
* consumes the previous set. 0 on success. */
int tp_proc_img_set_select(Pointer proc, int set);
/** Record the active set's ready event on the copy stream ("uploads landed") - the
* CPU-conditioned fallback; the GPU path's async conditioning records it itself. */
int tp_proc_img_set_ready_record(Pointer proc);
/** Async in-place conditioning of the ACTIVE image set on the copy stream (production
* float two-map kernel); records the set's ready event after the kernel - ONE event
* = uploaded+conditioned. No host synchronization. 0 on success. */
int tp_proc_exec_conditioning_async(Pointer proc);
/** D5b launch half of tp_proc_exec_pose_scene_dp: same inputs, GPU-side stream-wait on
* the active set's ready event instead of the host fence, raw enqueue with no context
* sync - returns 1 immediately while the chain runs (ONE chain in flight;
* wantTrace/wantMstats fix what the later collect may read). */
int tp_proc_exec_pose_scene_dp_launch(Pointer proc,
float[] refMeta, float[] refRadial,
float[] refRbr, int refRbrLen, float[] refErs, int refErsLen,
float[] sceneMeta, float[] sceneRadial,
float[] sceneRbr, int sceneRbrLen, float[] sceneErs, int sceneErsLen,
float[] poseVectors, int[] taskMap, float[] taskCenters,
int numCycles, float lambda, float pureWeight, float rmsDiff,
int numTiles, int corrSlot, int ntileShift,
int numComponents, int useEigen,
float eigMinSqrt, float eigMaxSqrt,
float minConfidence, int sameWeights,
int prepareLight,
float[] centers, float[] pullTargets, float[] paramRegweights,
float[] vector,
int wantTrace, int wantMstats);
/** D5b collect half: wait for the launched chain (implicitly - the first D2H) and fetch
* the end-of-scene results; returns the last cycle's corr-row count exactly like the
* blocking entry (or a negative). */
int tp_proc_exec_pose_scene_dp_collect(Pointer proc,
float[] trace, int[] mstats, float[] result,
float[] vectorOut, float[] prepOut, int[] stats);
/** Deterministic double normal-equation products for the lean pose LMA.
* jt is parameter-major [numParams][numValues]; out is row-major H followed by b.
* Damping and the small solve remain in Java. 0 on success. */
......
......@@ -39,6 +39,7 @@ public class CuasRtParameters {
public int pose_lma_debug = 0; // inner-LMA debug level, passed to the inner runLma (max with the run's own debugLevel): 0 = quiet (default; ALSO gates the per-scene "CuasPoseRT cycles:" trace line - >=1 to print it); 2 = one line PER INNER STEP ("LMA step N: {good,done} full RMS= ... pure RMS= ... + lambda=...") - the convergence trajectory INSIDE each outer cycle; 3-4 = progressively deeper solver dumps (parameter vectors, matrices). VERBOSE at >=2 - use for short investigation runs, not production. // By Claude on 07/12/2026, trace gate 07/13
public int pose_freeze_cycle = 0; // rung C3/C3b FALSIFIED 07/16 (freeze@1: az +48% roll +100%; freeze@2: az +30% roll +54%, NOT-settled 51->228): conditioning re-derivation matters through ALL cycles. 0 = never freeze = per-cycle (DEFAULT, restores C2 quality). Mechanism kept validated (bit-exact light path) for DP-era experiments (e.g. previous-scene conditioning). Was: freeze the lean-pose LMA conditioning (weights/eigen/selection/pull/reg) at this FULL prepare count; later cycles run light (fresh offsets only). 0 = never freeze = re-derive every cycle (highest accuracy, the pre-C3 behavior); 1 = freeze at cycle 1 (FAILED its gate: blurriest-pose conditioning, azimuth +48%); 2 = freeze at cycle 2 (post-first-correction measurement ~= converged). Keeps the high-accuracy opportunity selectable at runtime (Andrey 07/16). // By Claude on 07/16/2026
public boolean pose_scene_dp = true; // DP rung D4 (ladder ratified 07/16/2026): run the FULL per-scene lean pose solve as ONE device entry (pose_scene_dp parent: pose_cycles x [measure chain -> full prepare -> resident LMA step -> commit], self-chained on-device, one end-of-scene readback). Scene 0 always runs the host-driven (B3) loop to seed the resident per-sequence state AND is replayed through the DP entry as a one-shot bit-exact oracle; FAIL disables DP for the run (fail-safe). Requires JNA backend + uniform MB + fixed cycles + inner cap 1 + freeze 0; ineligible scenes/configs fall back to the host-driven loop automatically. OFF = the validated B3 host-driven loop (the A/B knob). // By Claude on 07/17/2026
public boolean pose_h2d_overlap = true; // D5 overlap rung (design ratified 07/17/2026): while scene N's DP entry runs on-device, condition scene N+1 into the OTHER device image-set ring slot (staged H2D + conditioning kernel on the copy stream) - the ~5.6 ms/scene conditioning+H2D stage hides under the ~12.5 ms chain. Requires pose_scene_dp + the preloaded RT harness (pose_preload); any overlap failure reverts to the serial order for the rest of the run (fail-safe). Bytes identical by construction (same kernels, different buffer). OFF = the serial condition-then-solve order (the A/B knob). // By Claude on 07/17/2026
public boolean rend_test = false; // RT full-render product (CuasRender.testRenderSequence): per scene raw /jp4/ -> conditionSceneToGpu -> virtual-grid render at BORROWED stored pose+ERS rates -> -CUAS-RT-RENDER hyperstack [s00..s15, merged][scenes], comparable to -CUAS-INDIVIDUAL/MERGED-CUAS-DBG. NON-exclusive: runs AFTER the pose stage when both are ON (the pose stage is part of the RT cycle - Andrey 07/06/2026). // By Claude on 07/05/2026
public boolean log_test = false; // LoG A/B with the RT render (CuasRender): also save -CUAS-RT-RENDER-LOG-ORACLE (product convolved with the EXISTING pre-DNN pixel LoG, LINEAR alpha=1) and -CUAS-RT-RENDER-LOG-FOLDED (same render, LoG folded into the GPU aberration kernels via CuasLogFold - no pixel convolution; originals restored). The aberration+LoG kernel-fold validation (RT-seed step 1b). // By Claude on 07/06/2026
public boolean log_ident = false; // LoG ISOLATION test with the RT render (CuasRender): render with IDENTITY kernels (-CUAS-RT-RENDER-ID, aberrations skipped), pixel-LoG it (-ID-LOG-ORACLE), and render with PURE-LoG kernels (-LOG-ONLY). If -LOG-ONLY vs -ID-LOG-ORACLE residual persists, the fold/L0 is wrong; if it collapses to the wrap floor, the aberration-kernel interaction was responsible (Andrey 07/06/2026). // By Claude on 07/06/2026
......@@ -173,6 +174,8 @@ public class CuasRtParameters {
"Freeze lean-pose LMA weights/eigen at this full-prepare count; later cycles reuse them (light prepare). 0 = never (re-derive every cycle, highest accuracy); 2 = recommended (post-first-correction measurement); 1 = falsified (blurry cycle-1 conditioning).");
gd.addCheckbox ("Pose scene as ONE GPU DP entry", this.pose_scene_dp, // By Claude on 07/17/2026
"DP rung D4: the whole per-scene pose solve (cycles x measure+prepare+LMA step) as ONE device entry, one end-of-scene readback. Scene 0 runs the host-driven loop and is replayed through the DP entry as a one-shot bit-exact oracle; FAIL disables DP for the run. Ineligible configs (JCuda, non-uniform MB, convergence exit, inner cap != 1, freeze != 0, debug saves) fall back automatically. OFF = the host-driven (B3) loop.");
gd.addCheckbox ("Overlap next-scene conditioning+H2D with pose DP", this.pose_h2d_overlap, // By Claude on 07/17/2026
"D5 overlap: while scene N's DP entry runs, condition scene N+1 into the other device image-set ring slot on the copy stream - the conditioning+H2D stage hides under the chain (~20.9 -> ~15.5 ms/scene). Requires the DP entry + the preloaded harness; any failure reverts to the serial order for the rest of the run. OFF = serial condition-then-solve (the A/B knob).");
gd.addMessage("=== RT render diagnostics ==="); // By Codex on 07/15/2026
gd.addCheckbox ("CUAS RT render (full product)", this.rend_test, // By Claude on 07/05/2026
......@@ -369,6 +372,7 @@ public class CuasRtParameters {
this.pose_lma_debug =(int) gd.getNextNumber(); // By Claude on 07/12/2026
this.pose_freeze_cycle =(int) gd.getNextNumber(); // By Claude on 07/16/2026
this.pose_scene_dp = gd.getNextBoolean(); // By Claude on 07/17/2026
this.pose_h2d_overlap = gd.getNextBoolean(); // By Claude on 07/17/2026
this.rend_test = gd.getNextBoolean(); // By Claude on 07/05/2026
this.log_test = gd.getNextBoolean(); // By Claude on 07/06/2026
this.log_ident = gd.getNextBoolean(); // By Claude on 07/06/2026
......@@ -479,6 +483,7 @@ public class CuasRtParameters {
properties.setProperty(prefix+"pose_lma_debug", this.pose_lma_debug+""); // int // By Claude on 07/12/2026
properties.setProperty(prefix+"pose_freeze_cycle", this.pose_freeze_cycle+""); // int // By Claude on 07/16/2026
properties.setProperty(prefix+"pose_scene_dp", this.pose_scene_dp+""); // boolean // By Claude on 07/17/2026
properties.setProperty(prefix+"pose_h2d_overlap",this.pose_h2d_overlap+"");// boolean // By Claude on 07/17/2026
properties.setProperty(prefix+"rend_test", this.rend_test+""); // boolean // By Claude on 07/05/2026
properties.setProperty(prefix+"log_test", this.log_test+""); // boolean // By Claude on 07/06/2026
properties.setProperty(prefix+"log_ident", this.log_ident+""); // boolean // By Claude on 07/06/2026
......@@ -589,6 +594,7 @@ public class CuasRtParameters {
if (properties.getProperty(prefix+"pose_lma_debug")!=null) this.pose_lma_debug=Integer.parseInt(properties.getProperty(prefix+"pose_lma_debug")); // By Claude on 07/12/2026
if (properties.getProperty(prefix+"pose_freeze_cycle")!=null) this.pose_freeze_cycle=Integer.parseInt(properties.getProperty(prefix+"pose_freeze_cycle")); // By Claude on 07/16/2026
if (properties.getProperty(prefix+"pose_scene_dp")!=null) this.pose_scene_dp=Boolean.parseBoolean(properties.getProperty(prefix+"pose_scene_dp")); // By Claude on 07/17/2026
if (properties.getProperty(prefix+"pose_h2d_overlap")!=null) this.pose_h2d_overlap=Boolean.parseBoolean(properties.getProperty(prefix+"pose_h2d_overlap")); // By Claude on 07/17/2026
if (properties.getProperty(prefix+"rend_test")!=null) this.rend_test=Boolean.parseBoolean(properties.getProperty(prefix+"rend_test")); // By Claude on 07/05/2026
if (properties.getProperty(prefix+"log_test")!=null) this.log_test=Boolean.parseBoolean(properties.getProperty(prefix+"log_test")); // By Claude on 07/06/2026
if (properties.getProperty(prefix+"log_ident")!=null) this.log_ident=Boolean.parseBoolean(properties.getProperty(prefix+"log_ident")); // By Claude on 07/06/2026
......@@ -702,6 +708,7 @@ public class CuasRtParameters {
cp.pose_lma_debug = this.pose_lma_debug; // By Claude on 07/12/2026
cp.pose_freeze_cycle = this.pose_freeze_cycle; // By Claude on 07/16/2026
cp.pose_scene_dp = this.pose_scene_dp; // By Claude on 07/17/2026
cp.pose_h2d_overlap = this.pose_h2d_overlap; // By Claude on 07/17/2026
cp.rend_test = this.rend_test; // By Claude on 07/05/2026
cp.log_test = this.log_test; // By Claude on 07/06/2026
cp.log_ident = this.log_ident; // By Claude on 07/06/2026
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment