Skip to main content
  • Home
  • Development
  • Documentation
  • Donate
  • Operational login
  • Browse the archive

swh logo
SoftwareHeritage
Software
Heritage
Archive
Features
  • Search

  • Downloads

  • Save code now

  • Add forge now

  • Help

https://github.com/ldyken53/TVCG-progiso
31 October 2024, 16:40:01 UTC
  • Code
  • Branches (3)
  • Releases (0)
  • Visits
    • Branches
    • Releases
    • HEAD
    • refs/heads/main
    • refs/heads/master
    • refs/heads/npm
    No releases to show
  • e9f1060
  • /
  • src
  • /
  • volume_raycaster.js
Raw File Download
Take a new snapshot of a software origin

If the archived software origin currently browsed is not synchronized with its upstream version (for instance when new commits have been issued), you can explicitly request Software Heritage to take a new snapshot of it.

Use the form below to proceed. Once a request has been submitted and accepted, it will be processed as soon as possible. You can then check its processing state by visiting this dedicated page.
swh spinner

Processing "take a new snapshot" request ...

To reference or cite the objects present in the Software Heritage archive, permalinks based on SoftWare Hash IDentifiers (SWHIDs) must be used.
Select below a type of object currently browsed in order to display its associated SWHID and permalink.

  • content
  • directory
  • revision
  • snapshot
origin badgecontent badge
swh:1:cnt:bf27134b168dd32a6140a9f171aada56b5823e3e
origin badgedirectory badge
swh:1:dir:9a5575fd388597f53212908dc146c18fc621d17e
origin badgerevision badge
swh:1:rev:9b4d063dbd517c8db6196fcfba97d7a4442af2c0
origin badgesnapshot badge
swh:1:snp:6dbeb1fbe859f48945dd22bd530e60eb1a5ac9b6

This interface enables to generate software citations, provided that the root directory of browsed objects contains a citation.cff or codemeta.json file.
Select below a type of object currently browsed in order to generate citations for them.

  • content
  • directory
  • revision
  • snapshot
Generate software citation in BibTex format (requires biblatex-software package)
Generating citation ...
Generate software citation in BibTex format (requires biblatex-software package)
Generating citation ...
Generate software citation in BibTex format (requires biblatex-software package)
Generating citation ...
Generate software citation in BibTex format (requires biblatex-software package)
Generating citation ...
Tip revision: 9b4d063dbd517c8db6196fcfba97d7a4442af2c0 authored by Landon Dyken on 11 June 2024, 17:43:45 UTC
Handle all buffer management manually rather than automatically allocating with ONNX
Tip revision: 9b4d063
volume_raycaster.js
import { ExclusiveScanPipeline, alignTo } from "./exclusive_scan";
import { StreamCompact } from "./stream_compact";
import { RadixSorter } from "./radix_sort_by_key";
import { LRUCache } from "./lru_cache";
import * as shaders from "./embedded_shaders";
import { buildPushConstantsBuffer } from "./util";

export var VolumeRaycaster = function(
    device, width, height, recordVisibleBlocksUI, enableSpeculationUI, startSpecCount, imageTensorBuffer) {
    this.device = device;
    this.scanPipeline = new ExclusiveScanPipeline(device);
    this.streamCompact = new StreamCompact(device);
    // Number of blocks visible for the current pass
    this.numVisibleBlocks = 0;
    // Number of blocks decompressed for the current pass
    this.newDecompressed = 0;

    this.renderComplete = false;

    this.width = width;
    this.height = height;
    this.startSpecCount = startSpecCount;

    // Record visible blocks will optionally track the total % of blocks that were active or
    // visible while rendering the surface. This is just needed to provide this statistic for
    // the paper and is computed on the host by just reading back the blockVisible buffer and
    // or'ing it with the previous pass's one to accumulate the total block visible list
    // without double-counting
    this.recordVisibleBlocksUI = recordVisibleBlocksUI;

    // For testing/demo/benchmarking of enable/disable speculation
    this.enableSpeculationUI = enableSpeculationUI;

    // Each pass appends its performance stats to the perfStats array,
    // this is reset for each new isovalue
    this.perfStats = [];

    // Max dispatch size for more computationally heavy kernels
    // which might hit TDR on lower power devices
    this.maxDispatchSize = device.limits.maxComputeWorkgroupsPerDimension;

    this.computeBlockRangeBGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 2,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
        ],
    });
    this.pushConstantS1B0DynamicLayout = device.createBindGroupLayout({
        entries: [{
            binding: 0,
            visibility: GPUShaderStage.COMPUTE,
            buffer: {type: "uniform", hasDynamicOffset: true}
        }]
    });
    this.computeBlockRangePipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts:
                [this.computeBlockRangeBGLayout, this.pushConstantS1B0DynamicLayout],
        }),
        compute: {
            module: device.createShaderModule({
                code: shaders.zfp_compute_block_range_comp_spv,
            }),
            entryPoint: "main",
        },
    });

    this.computeVoxelRangeBGLayout = device.createBindGroupLayout({
        entries: [{
            binding: 0,
            visibility: GPUShaderStage.COMPUTE,
            buffer: {
                type: "storage",
            }
        }]
    });
    this.computeVoxelRangePipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts: [
                this.computeBlockRangeBGLayout,
                this.pushConstantS1B0DynamicLayout,
                this.computeVoxelRangeBGLayout
            ]
        }),
        compute: {
            module: device.createShaderModule({
                code: shaders.compute_voxel_range_comp_spv,
            }),
            entryPoint: "main",
        }
    });

    this.computeCoarseCellRangeBGLayout = device.createBindGroupLayout({
        entries: [
            {binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {type: "uniform", hasDynamicOffset: true}
            },
            {binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}}
        ]
    });
    this.computeCoarseCellRangePipeline = device.createComputePipeline({
        layout: device.createPipelineLayout(
            {bindGroupLayouts: [this.computeCoarseCellRangeBGLayout]}),
        compute: {
            module: device.createShaderModule({code: shaders.compute_coarse_cell_range_comp_spv}),
            entryPoint: "main"
        }
    });

    this.decompressBlocksBGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 2,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 3,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 4,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
        ],
    });
    this.ub1binding0BGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "uniform",
                }
            },
        ],
    });

    this.decompressBlocksPipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts: [
                this.decompressBlocksBGLayout,
                this.ub1binding0BGLayout,
            ],
        }),
        compute: {
            module: device.createShaderModule({
                code: shaders.zfp_decompress_block_comp_spv,
            }),
            entryPoint: "main",
        },
    });

    // Set up compute initial rays pipeline
    this.viewParamBuf = device.createBuffer({
        // mat4, 2 vec4's, a float, 2 uint + some extra to align
        size: 32 * 4,
        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });

    // We'll need a max of width * height RayInfo structs in the buffer,
    // so just allocate it once up front
    this.rayInformationBuffer = device.createBuffer({
        size: this.width * this.height * 16,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
    });

    // We need width * height RayIDs for speculation,
    // with ray indices repeated as speculation occurs
    this.speculativeRayIDBuffer = device.createBuffer({
        size: this.width * this.height * this.startSpecCount * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
    });

    this.rayRGBZBuffer = device.createBuffer({
        size: this.width * this.height * this.startSpecCount * 2 * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
    });

    this.imageTensorBuffer = imageTensorBuffer;

    // Each ray stores 2 iterator states, the coarse one followed by the fine one.
    // Each state is 32b
    this.gridIteratorBuffer = device.createBuffer({
        size: this.width * this.height * 8 * 4,
        usage: GPUBufferUsage.STORAGE,
    });

    this.resetRaysBGLayout = device.createBindGroupLayout({
        entries: [
            {binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
            {binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}}
        ]
    });

    this.resetRaysPipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts: [
                this.resetRaysBGLayout,
            ],
        }),
        compute: {
            module: device.createShaderModule({
                code: shaders.reset_rays_comp_spv,
            }),
            entryPoint: "main",
        },
    });

    this.computeInitialRaysBGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.VERTEX,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.FRAGMENT,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 2,
                visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 3,
                visibility: GPUShaderStage.FRAGMENT,
                buffer: {
                    type: "storage",
                }
            },
        ],
    });

    // Specify vertex data for compute initial rays
    this.dataBuf = device.createBuffer({
        size: 12 * 3 * 3 * 4,
        usage: GPUBufferUsage.VERTEX,
        mappedAtCreation: true,
    });
    new Float32Array(this.dataBuf.getMappedRange()).set([
        1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1,
        1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1,
        0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1,
        0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0,
    ]);
    this.dataBuf.unmap();

    // Setup render outputs
    var renderTargetFormat = "rgba8unorm";
    this.renderTarget = this.device.createTexture({
        size: [this.width, this.height, 1],
        format: renderTargetFormat,
        usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING |
                   GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST
    });
    this.renderTargetCopy = this.device.createTexture({
        size: [this.width, this.height, 1],
        format: renderTargetFormat,
        usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
    });

    this.renderTargetDebugBGLayout = this.device.createBindGroupLayout({
        entries: [{
            binding: 0,
            visibility: GPUShaderStage.COMPUTE,
            storageTexture: {access: "write-only", format: renderTargetFormat}
        }]
    });
    this.renderTargetDebugBG = this.device.createBindGroup({
        layout: this.renderTargetDebugBGLayout,
        entries: [{binding: 0, resource: this.renderTarget.createView()}]
    });

    this.initialRaysPipeline = device.createRenderPipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts: [this.computeInitialRaysBGLayout],
        }),
        vertex: {
            module: device.createShaderModule({code: shaders.compute_initial_rays_vert_spv}),
            entryPoint: "main",
            buffers: [
                {
                    arrayStride: 3 * 4,
                    attributes: [
                        {
                            format: "float32x3",
                            offset: 0,
                            shaderLocation: 0,
                        },
                    ],
                },
            ],
        },
        fragment: {
            module: device.createShaderModule({code: shaders.compute_initial_rays_frag_spv}),
            entryPoint: "main",
            targets: [
                {
                    format: renderTargetFormat,
                    writeMask: 0,
                },
            ],
        },
        primitive: {
            topology: 'triangle-list',
            cullMode: "front",
        }
    });

    this.initialRaysPassDesc = {
        colorAttachments: [
            {
                view: this.renderTarget.createView(),
                loadOp: "clear",
                clearValue: [1.0, 1.0, 1.0, 1],
                storeOp: "store"
            },
        ]
    };

    this.depthCompositeBGLayout = device.createBindGroupLayout({
        entries: [
            {binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
            {binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {
                binding: 3,
                visibility: GPUShaderStage.COMPUTE,
                storageTexture: {access: "write-only", format: renderTargetFormat}
            },
            {binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
        ]
    });
    this.depthCompositeBG1Layout = device.createBindGroupLayout({
        entries: [
            {binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}}, 
            {binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}}
        ]
    });

    this.depthCompositePipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts: [this.depthCompositeBGLayout, this.depthCompositeBG1Layout],
        }),
        compute: {
            module: device.createShaderModule({
                code: shaders.depth_composite_comp_spv,
            }),
            entryPoint: "main",
        },
    });

    this.resetSpeculativeIDsBGLayout = device.createBindGroupLayout({
        entries: [
            {binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
            {binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
        ]
    });

    this.resetSpeculativeIDsPipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts: [
                this.resetSpeculativeIDsBGLayout,
            ],
        }),
        compute: {
            module: device.createShaderModule({
                code: shaders.reset_speculative_ids_comp_spv,
            }),
            entryPoint: "main",
        },
    });

    this.macroTraverseBGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 2,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                // Also pass the render target for debugging
                binding: 3,
                visibility: GPUShaderStage.COMPUTE,
                storageTexture: {access: "write-only", format: renderTargetFormat}
            },
            {
                binding: 4,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 5,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 6,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 7,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            }
        ],
    });
    this.macroTraverseRangesBGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            }
        ]
    });

    this.macroTraversePipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts: [this.macroTraverseBGLayout, this.macroTraverseRangesBGLayout],
        }),
        compute: {
            module: device.createShaderModule({
                code: shaders.macro_traverse_comp_spv,
            }),
            entryPoint: "main",
        },
    });

    this.resetBlockActiveBGLayout = device.createBindGroupLayout({
        entries: [
            {binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
            {binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}}
        ]
    });
    this.resetBlockActivePipeline = device.createComputePipeline({
        layout:
            device.createPipelineLayout({bindGroupLayouts: [this.resetBlockActiveBGLayout]}),
        compute: {
            module: device.createShaderModule({code: shaders.reset_block_active_comp_spv}),
            entryPoint: "main"
        }
    });

    this.resetBlockNumRaysBGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {type: "uniform", hasDynamicOffset: true}
            },
            {binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}}
        ]
    });
    this.resetBlockNumRaysPipeline = device.createComputePipeline({
        layout:
            device.createPipelineLayout({bindGroupLayouts: [this.resetBlockNumRaysBGLayout]}),
        compute: {
            module: device.createShaderModule({code: shaders.reset_block_num_rays_comp_spv}),
            entryPoint: "main"
        }
    });

    this.LODThresholdBuf = device.createBuffer({
        size: 4,
        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });

    this.markBlockActiveBGLayout = device.createBindGroupLayout({
        entries: [
            {binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
            {binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
            {binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
            {binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
        ]
    });
    this.markBlockActivePipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts: [this.markBlockActiveBGLayout, this.renderTargetDebugBGLayout]
        }),
        compute: {
            module: device.createShaderModule({code: shaders.mark_block_active_wgsl_spv}),
            entryPoint: "main"
        }
    });

    this.countBlockRaysBGLayout = device.createBindGroupLayout({
        entries: [
            {binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
            {binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
        ]
    });
    this.countBlockRaysPipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts: [this.countBlockRaysBGLayout],
        }),
        compute: {
            module: device.createShaderModule({code: shaders.count_block_rays_wgsl_spv}),
            entryPoint: "main"
        }
    });

    this.debugViewBlockRayCountsBGLayout = device.createBindGroupLayout({
        entries: [
            {binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: {type: "uniform"}},
            {binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: {type: "storage"}},
            {
                binding: 3,
                visibility: GPUShaderStage.COMPUTE,
                storageTexture: {access: "write-only", format: renderTargetFormat}
            }
        ]
    });
    this.debugViewBlockRayCountsPipeline = device.createComputePipeline({
        layout: device.createPipelineLayout(
            {bindGroupLayouts: [this.debugViewBlockRayCountsBGLayout]}),
        compute: {
            module: device.createShaderModule({code: shaders.debug_view_rays_per_block_comp_spv}),
            entryPoint: "main"
        }
    });

    // Intermediate buffers for sorting ray IDs using their block ID as the key
    this.radixSorter = new RadixSorter(device);
    this.rayIDBuffer = device.createBuffer({
        size: this.radixSorter.getAlignedSize(this.width * this.height * this.startSpecCount) * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
    });
    this.compactSpeculativeIDBuffer = device.createBuffer({
        size: this.radixSorter.getAlignedSize(this.width * this.height * this.startSpecCount) * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
    });
    this.rayBlockIDBuffer = device.createBuffer({
        size: this.radixSorter.getAlignedSize(this.width * this.height * this.startSpecCount) * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
    });
    this.compactRayBlockIDBuffer = device.createBuffer({
        size: this.radixSorter.getAlignedSize(this.width * this.height * this.startSpecCount) * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
    });
    this.rayActiveBuffer = device.createBuffer({
        size: this.width * this.height * this.startSpecCount * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
    });
    this.rayActiveCompactOffsetBuffer = device.createBuffer({
        size: this.scanPipeline.getAlignedSize(this.width * this.height * this.startSpecCount) * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
    });
    this.rayAfterActiveBuffer = device.createBuffer({
        size: this.width * this.height * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
    });
    this.speculativeRayOffsetBuffer = device.createBuffer({
        size: this.scanPipeline.getAlignedSize(this.width * this.height) * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
    });
    this.scanRayActive = this.scanPipeline.prepareGPUInput(
        this.rayActiveCompactOffsetBuffer,
        this.scanPipeline.getAlignedSize(this.width * this.height * this.startSpecCount));
    this.scanRayAfterActive = this.scanPipeline.prepareGPUInput(
        this.speculativeRayOffsetBuffer,
        this.scanPipeline.getAlignedSize(this.width * this.height));

    this.writeRayAndBlockIDBGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 2,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
        ],
    });
    this.writeRayAndBlockIDPipeline = device.createComputePipeline({
        layout:
            device.createPipelineLayout({bindGroupLayouts: [this.writeRayAndBlockIDBGLayout]}),
        compute: {
            module: device.createShaderModule({code: shaders.write_ray_and_block_id_comp_spv}),
            entryPoint: "main",
        }
    });

    this.markRayActiveBGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 2,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
        ],
    });
    this.markRayActivePipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({bindGroupLayouts: [this.markRayActiveBGLayout]}),
        compute: {
            module: device.createShaderModule({code: shaders.mark_ray_active_comp_spv}),
            entryPoint: "main",
        }
    });

    this.colorActiveRaysBGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 2,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 3,
                visibility: GPUShaderStage.COMPUTE,
                texture: { viewDimension: "2d" }
            },
            {
                binding: 4,
                visibility: GPUShaderStage.COMPUTE,
                storageTexture: { access: "write-only", format: renderTargetFormat }
            },
        ]
    });
    this.colorActiveRaysPipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({bindGroupLayouts: [this.colorActiveRaysBGLayout]}),
        compute: {
            module: device.createShaderModule({code: shaders.color_active_rays_comp_spv}),
            entryPoint: "main",
        }
    });

    this.combineBlockInformationBGLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 2,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 3,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 4,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            }
        ]
    });
    this.combineBlockInformationPipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts:
                [this.combineBlockInformationBGLayout, this.pushConstantS1B0DynamicLayout]
        }),
        compute: {
            module: device.createShaderModule({code: shaders.combine_block_information_comp_spv}),
            entryPoint: "main"
        }
    });

    this.rtBlocksPipelineBG0Layout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 2,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
        ]
    });
    this.rtBlocksPipelineBG1Layout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "uniform",
                }
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 2,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 3,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 4,
                visibility: GPUShaderStage.COMPUTE,
                storageTexture: {access: "write-only", format: renderTargetFormat}
            },
            {
                binding: 5,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
            {
                binding: 6,
                visibility: GPUShaderStage.COMPUTE,
                buffer: {
                    type: "storage",
                }
            },
        ]
    });

    this.raytraceBlocksPipeline = device.createComputePipeline({
        layout: device.createPipelineLayout({
            bindGroupLayouts: [
                this.rtBlocksPipelineBG0Layout,
                this.rtBlocksPipelineBG1Layout,
                this.pushConstantS1B0DynamicLayout
            ]
        }),
        compute: {
            module: device.createShaderModule({code: shaders.raytrace_active_block_comp_spv}),
            entryPoint: "main"
        }
    });
};

VolumeRaycaster.prototype.setCompressedVolume =
    async function(volume, compressionRate, volumeDims, volumeScale) {
    // Upload the volume
    this.volumeDims = volumeDims;
    this.paddedDims = [
        alignTo(volumeDims[0], 4),
        alignTo(volumeDims[1], 4),
        alignTo(volumeDims[2], 4),
    ];
    this.blockGridDims =
        [this.paddedDims[0] / 4, this.paddedDims[1] / 4, this.paddedDims[2] / 4];
    this.totalBlocks = (this.paddedDims[0] * this.paddedDims[1] * this.paddedDims[2]) / 64;

    this.coarseGridDims = [
        alignTo(this.blockGridDims[0], 4) / 4,
        alignTo(this.blockGridDims[1], 4) / 4,
        alignTo(this.blockGridDims[2], 4) / 4,
    ];
    this.totalCoarseCells =
        (this.coarseGridDims[0] * this.coarseGridDims[1] * this.coarseGridDims[2]);

    const groupThreadCount = 32;
    this.numWorkGroups = Math.ceil(this.totalBlocks / groupThreadCount);
    var cacheInitialSize = Math.ceil(this.totalBlocks * 0.01);

    this.lruCache = new LRUCache(this.device,
                                 this.scanPipeline,
                                 this.streamCompact,
                                 cacheInitialSize,
                                 64 * 4,
                                 this.totalBlocks);

    this.volumeInfoBuffer = this.device.createBuffer({
        size: 16 * 4,
        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
        mappedAtCreation: true,
    });
    {
        var mapping = this.volumeInfoBuffer.getMappedRange();
        var maxBits = (1 << (2 * 3)) * compressionRate;
        var buf = new Uint32Array(mapping);
        buf.set(volumeDims);
        buf.set(this.paddedDims, 4);
        buf.set([maxBits], 12);
        buf.set([this.width], 14);
        buf.set([this.height], 15);

        var buf = new Float32Array(mapping);
        buf.set(volumeScale, 8);
    }
    this.volumeInfoBuffer.unmap();

    var compressedBuffer = this.device.createBuffer({
        size: alignTo(volume.byteLength, 4),
        usage: GPUBufferUsage.STORAGE,
        mappedAtCreation: true,
    });
    new Uint8Array(compressedBuffer.getMappedRange()).set(volume);
    compressedBuffer.unmap();
    this.compressedBuffer = compressedBuffer;

    this.uploadIsovalueBuf = this.device.createBuffer({
        size: 4,
        usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
    });

    this.uploadLODBuf = this.device.createBuffer({
        size: 4,
        usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
    });

    await this.computeBlockRanges();

    this.blockVisibleBuffer = this.device.createBuffer(
        {size: 4 * this.totalBlocks, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC});

    // Buffers for use when compacting down IDs of the active blocks
    // TODO: could do a bit better and filter neighbor blocks out too by just marking
    // visible blocks in a separate buffer (i.e., ones a ray is immediately in).
    // This set will include neighbors for now
    this.blockVisibleCompactOffsetBuffer = this.device.createBuffer({
        size: 4 * this.scanPipeline.getAlignedSize(this.totalBlocks),
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
    });

    // blockActive and blockVisibleCompactOffsetBuffer can alias each other as they are used at
    // different times in the pipeline
    this.blockActiveBuffer = this.blockVisibleCompactOffsetBuffer;

    this.scanBlockVisibleOffsets =
        this.scanPipeline.prepareGPUInput(this.blockVisibleCompactOffsetBuffer,
                                          this.scanPipeline.getAlignedSize(this.totalBlocks));

    this.resetRaysBG = this.device.createBindGroup({
        layout: this.resetRaysBGLayout,
        entries: [
            {binding: 0, resource: {buffer: this.rayInformationBuffer}},
            {binding: 1, resource: {buffer: this.volumeInfoBuffer}},
            {binding: 2, resource: {buffer: this.rayBlockIDBuffer}},
            {binding: 3, resource: {buffer: this.imageTensorBuffer}},
            {binding: 4, resource: {buffer: this.viewParamBuf}}
        ]
    });

    this.resetSpeculativeIDsBG = this.device.createBindGroup({
        layout: this.resetSpeculativeIDsBGLayout,
        entries: [
            {binding: 0, resource: {buffer: this.volumeInfoBuffer}},
            {binding: 1, resource: {buffer: this.speculativeRayIDBuffer}},
            {binding: 2, resource: {buffer: this.rayRGBZBuffer}},
            {binding: 3, resource: {buffer: this.rayBlockIDBuffer}},
        ]
    });

    this.depthCompositeBG = this.device.createBindGroup({
        layout: this.depthCompositeBGLayout,
        entries: [
            {binding: 0, resource: {buffer: this.viewParamBuf}},
            {binding: 1, resource: {buffer: this.speculativeRayIDBuffer}},
            {binding: 2, resource: {buffer: this.rayRGBZBuffer}},
            {binding: 3, resource: this.renderTarget.createView()},
            {binding: 4, resource: {buffer: this.volumeInfoBuffer}},
        ]
    });

    this.depthCompositeBG1 = this.device.createBindGroup({
        layout: this.depthCompositeBG1Layout,
        entries: [
            {binding: 0, resource: {buffer: this.rayInformationBuffer}},
            {binding: 1, resource: {buffer: this.imageTensorBuffer}}
        ]
    });

    // this.initSpeculativeIDsBG = this.device.createBindGroup({
    //     layout: this.initSpeculativeIDsBGLayout,
    //     entries: [
    //         {binding: 0, resource: {buffer: this.volumeInfoBuffer}},
    //         {binding: 1, resource: {buffer: this.viewParamBuf}},
    //         {binding: 2, resource: {buffer: this.speculativeIDBuffer}},
    //         {binding: 3, resource: {buffer: this.speculativeRayOffsetBuffer}},
    //     ]
    // });

    this.initialRaysBindGroup = this.device.createBindGroup({
        layout: this.computeInitialRaysBGLayout,
        entries: [
            {
                binding: 0,
                resource: {
                    buffer: this.viewParamBuf,
                },
            },
            {
                binding: 1,
                resource: {
                    buffer: this.rayInformationBuffer,
                },
            },
            {
                binding: 2,
                resource: {
                    buffer: this.volumeInfoBuffer,
                },
            },
            {
                binding: 3,
                resource: {
                    buffer: this.rayBlockIDBuffer,
                },
            },
        ],
    });

    this.macroTraverseBindGroup = this.device.createBindGroup({
        layout: this.macroTraverseBGLayout,
        entries: [
            {
                binding: 0,
                resource: {
                    buffer: this.volumeInfoBuffer,
                },
            },
            {
                binding: 1,
                resource: {
                    buffer: this.viewParamBuf,
                },
            },
            {
                binding: 2,
                resource: {
                    buffer: this.rayInformationBuffer,
                },
            },
            {binding: 3, resource: this.renderTarget.createView()},
            {
                binding: 4,
                resource: {
                    buffer: this.gridIteratorBuffer,
                },
            },
            {
                binding: 5,
                resource: {
                    buffer: this.speculativeRayIDBuffer,
                },
            },
            {
                binding: 6,
                resource: {
                    buffer: this.speculativeRayOffsetBuffer,
                },
            },
            {
                binding: 7,
                resource: {
                    buffer: this.rayBlockIDBuffer,
                }
            }
        ],
    });

    this.resetBlockActiveBG = this.device.createBindGroup({
        layout: this.resetBlockActiveBGLayout,
        entries: [
            {binding: 0, resource: {buffer: this.volumeInfoBuffer}},
            {binding: 1, resource: {buffer: this.blockActiveBuffer}},
            {binding: 2, resource: {buffer: this.blockVisibleBuffer}},
        ]
    });

    this.markBlockActiveBG = this.device.createBindGroup({
        layout: this.markBlockActiveBGLayout,
        entries: [
            {binding: 0, resource: {buffer: this.volumeInfoBuffer}},
            {binding: 1, resource: {buffer: this.LODThresholdBuf}},
            {binding: 2, resource: {buffer: this.viewParamBuf}},
            {binding: 3, resource: {buffer: this.blockActiveBuffer}},
            {binding: 4, resource: {buffer: this.rayInformationBuffer}},
            {binding: 5, resource: {buffer: this.blockVisibleBuffer}},
            {binding: 6, resource: {buffer: this.rayBlockIDBuffer}},
        ]
    });

    this.writeRayAndBlockIDBG = this.device.createBindGroup({
        layout: this.writeRayAndBlockIDBGLayout,
        entries: [
            {binding: 0, resource: {buffer: this.volumeInfoBuffer}},
            {binding: 1, resource: {buffer: this.rayBlockIDBuffer}},
            {binding: 2, resource: {buffer: this.rayActiveBuffer}},
        ]
    });

    this.markRayActiveBG = this.device.createBindGroup({
        layout: this.markRayActiveBGLayout,
        entries: [
            {binding: 0, resource: {buffer: this.volumeInfoBuffer}},
            {binding: 1, resource: {buffer: this.rayInformationBuffer}},
            {binding: 2, resource: {buffer: this.rayAfterActiveBuffer}},
        ]
    });
    this.colorActiveRaysBG = this.device.createBindGroup({
        layout: this.colorActiveRaysBGLayout,
        entries: [
            {binding: 0, resource: {buffer: this.volumeInfoBuffer}},
            {binding: 1, resource: {buffer: this.rayAfterActiveBuffer}},
            {binding: 2, resource: {buffer: this.imageTensorBuffer}},
            {binding: 3, resource: this.renderTargetCopy.createView()},
            {binding: 4, resource: this.renderTarget.createView()}
        ]
    });
};

VolumeRaycaster.prototype.getMemoryStats = function() {
    // Data from this object
    var memUse = {
        units: "bytes",
        mc: {
            compressedData: this.compressedBuffer.size,
            voxelRanges: this.voxelRangesBuffer.size,
            coarseCellRanges: this.coarseCellRangesBuffer.size,

            // Every app will have view params, we can ignore it
            // viewParam: this.viewParamBuf.size,
            rays: this.rayInformationBuffer.size,
            rayPixelIDs: this.speculativeRayIDBuffer.size,
            sortedRayIDs: this.rayIDBuffer.size,
            compactPixelIDs: this.compactSpeculativeIDBuffer.size,
            rayBlockIDs: this.rayBlockIDBuffer.size,
            compactRayBlockIDs: this.compactRayBlockIDBuffer.size,
            rayActive: this.rayActiveBuffer.size,
            rayActiveCompactOffsets: this.rayActiveCompactOffsetBuffer.size,
            speculativeRayOffsets: this.speculativeRayOffsetBuffer.size,
            rayRGBZ: this.rayRGBZBuffer.size,

            gridIteratorState: this.gridIteratorBuffer.size,

            // block active aliases blockVisibleCompactOffsetBuffer because they aren't needed
            // at the same time in the pipeline
            // blockActive: this.blockActiveBuffer.size,
            blockVisible: this.blockVisibleBuffer.size,
            // This is really based on block visible, not active
            blockVisibleCompactOffsets: this.blockVisibleCompactOffsetBuffer.size,
            blockNumRays: this.blockNumRaysBuffer ? this.blockNumRaysBuffer.size : 0,
            blockRayOffsets: this.blockRayOffsetBuffer ? this.blockRayOffsetBuffer.size : 0,
            visibleBlockIDs: this.visibleBlockIDBuffer ? this.visibleBlockIDBuffer.size : 0,

            // Ignoring the combined block info buffer since this came from a binding count
            // limitation in WebGPU which I think has been addressed, we just haven't updated
            // the code, and it's just # active block size

            // combinedBlockInformation: this.combinedBlockInformationBuffer.size,

            // I think we can also ignore the cube vertices
            // cubeVertices: this.dataBuf.size
        },
        cache: {
            cache: this.lruCache.cacheSize * this.lruCache.elementSize,
            cachedItemSlots: this.lruCache.totalElements * 4,
            needsCaching: this.lruCache.totalElements * 4,
            needsCachingOffsets:
                this.scanPipeline.getAlignedSize(this.lruCache.totalElements) * 4,
            slotAge: this.lruCache.cacheSize * 4,
            slotAvailable: this.lruCache.cacheSize * 4,
            slotAvailableOffsets:
                this.scanPipeline.getAlignedSize(this.lruCache.cacheSize) * 4,
            slotAvailableIDs: this.lruCache.cacheSize * 4,
            slotItemIDs: this.lruCache.cacheSize * 4,
            cacheSizeBuf: 4,
        },
    };
    return memUse;
};

VolumeRaycaster.prototype.reportMemoryUse = function() {
    var formatBytes = function(count) {
        const giga = 1000000000;
        const mega = 1000000;
        const kilo = 1000;
        if (count > giga) {
            return (count / giga).toFixed(2) + " GB";
        } else if (count > mega) {
            return (count / mega).toFixed(2) + " MB";
        } else if (count > kilo) {
            return (count / kilo).toFixed(2) + " KB";
        }
        return count + " B";
    };

    var memUse = this.getMemoryStats();

    var totalMem = 0;
    var rcText = "Raycaster Data:<ul>";
    for (const prop in memUse.mc) {
        totalMem += memUse.mc[prop];
        rcText += "<li>" + prop + ": " + formatBytes(memUse.mc[prop]) + "</li>";
    }
    rcText += "</ul>";

    var cacheText = "LRU Cache Data:<ul>";
    for (const prop in memUse.cache) {
        totalMem += memUse.cache[prop];
        cacheText += "<li>" + prop + ": " + formatBytes(memUse.cache[prop]) + "</li>";
    }
    cacheText += "</ul>";
    return [rcText, cacheText, formatBytes(totalMem), memUse];
};

VolumeRaycaster.prototype.computeBlockRanges = async function() {
    // Note: this could be done by the server for us, but for this prototype
    // it's a bit easier to just do it here
    // Decompress each block and compute its value range, output to the blockRangesBuffer
    // BlockRangesBuffer = purely the ZFP block range
    // TODO: We don't need to keep this buffer long term actually
    var blockRangesBuffer = this.device.createBuffer({
        size: this.totalBlocks * 2 * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
    });
    // VoxelRangesBuffer = block range + neighbor cells for the dual grid
    this.voxelRangesBuffer = this.device.createBuffer({
        size: this.totalBlocks * 2 * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
    });
    // CoarseCellRanges = 4^3 blocks of ZFP blocks, including neighbor
    this.coarseCellRangesBuffer = this.device.createBuffer({
        size: this.totalCoarseCells * 2 * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
    });

    var bindGroup = this.device.createBindGroup({
        layout: this.computeBlockRangeBGLayout,
        entries: [
            {
                binding: 0,
                resource: {
                    buffer: this.compressedBuffer,
                }
            },
            {
                binding: 1,
                resource: {
                    buffer: this.volumeInfoBuffer,
                }
            },
            {
                binding: 2,
                resource: {
                    buffer: blockRangesBuffer,
                }
            }
        ]
    });
    this.voxelBindGroup = this.device.createBindGroup({
        layout: this.computeVoxelRangeBGLayout,
        entries: [{
            binding: 0,
            resource: {
                buffer: this.voxelRangesBuffer,
            }
        }]
    });

    this.macroTraverseRangesBG = this.device.createBindGroup({
        layout: this.macroTraverseRangesBGLayout,
        entries: [
            {
                binding: 0,
                resource: {
                    buffer: this.voxelRangesBuffer,
                },
            },
            {
                binding: 1,
                resource: {
                    buffer: this.coarseCellRangesBuffer,
                }
            }
        ]

    });

    const groupThreadCount = 32;
    var totalWorkGroups = Math.ceil(this.totalBlocks / groupThreadCount);

    var pushConstants = buildPushConstantsBuffer(this.device, totalWorkGroups);

    var blockIDOffsetBG = this.device.createBindGroup({
        layout: this.pushConstantS1B0DynamicLayout,
        entries: [{binding: 0, resource: {buffer: pushConstants.gpuBuffer, size: 4}}]
    });

    var commandEncoder = this.device.createCommandEncoder();
    var pass = commandEncoder.beginComputePass();

    // Decompress each block and compute its range
    pass.setPipeline(this.computeBlockRangePipeline);
    pass.setBindGroup(0, bindGroup);
    for (var i = 0; i < pushConstants.nOffsets; ++i) {
        pass.setBindGroup(1, blockIDOffsetBG, pushConstants.dynamicOffsets, i, 1);
        pass.dispatchWorkgroups(pushConstants.dispatchSizes[i], 1, 1);
    }
    pass.end();

    // Compute each block's range including its neighbors
    var pass = commandEncoder.beginComputePass();
    pass.setPipeline(this.computeVoxelRangePipeline);
    pass.setBindGroup(0, bindGroup);
    pass.setBindGroup(2, this.voxelBindGroup);
    for (var i = 0; i < pushConstants.nOffsets; ++i) {
        pass.setBindGroup(1, blockIDOffsetBG, pushConstants.dynamicOffsets, i, 1);
        pass.dispatchWorkgroups(pushConstants.dispatchSizes[i], 1, 1);
    }
    pass.end();

    var pass = commandEncoder.beginComputePass();
    // Enqueue pass to compute the coarse cell ranges
    var totalWorkGroupsCoarseCell = Math.ceil(this.totalCoarseCells / groupThreadCount);
    var coarsePushConstants = buildPushConstantsBuffer(this.device, totalWorkGroupsCoarseCell);
    var coarseRangeBG = this.device.createBindGroup({
        layout: this.computeCoarseCellRangeBGLayout,
        entries: [
            {
                binding: 0,
                resource: {
                    buffer: this.volumeInfoBuffer,
                }
            },
            {binding: 1, resource: {buffer: coarsePushConstants.gpuBuffer, size: 4}},
            {binding: 2, resource: {buffer: this.voxelRangesBuffer}},
            {binding: 3, resource: {buffer: this.coarseCellRangesBuffer}}
        ]
    });
    pass.setPipeline(this.computeCoarseCellRangePipeline);
    for (var i = 0; i < coarsePushConstants.nOffsets; ++i) {
        pass.setBindGroup(0, coarseRangeBG, coarsePushConstants.dynamicOffsets, i, 1);
        pass.dispatchWorkgroups(coarsePushConstants.dispatchSizes[i], 1, 1);
    }

    pass.end();
    this.device.queue.submit([commandEncoder.finish()]);
    await this.device.queue.onSubmittedWorkDone();

    /*
    {
        var dbgBuffer = this.device.createBuffer({
            size: blockRangesBuffer.size,
            usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
        });

        var commandEncoder = this.device.createCommandEncoder();
        commandEncoder.copyBufferToBuffer(blockRangesBuffer, 0, dbgBuffer, 0, dbgBuffer.size);
        this.device.queue.submit([commandEncoder.finish()]);
        await this.device.queue.onSubmittedWorkDone();

        await dbgBuffer.mapAsync(GPUMapMode.READ);

        var debugVals = new Float32Array(dbgBuffer.getMappedRange());

        for (var i = 0; i < 3; i++) {
            var blob = new Blob(['[' + debugVals.slice(i * debugVals.length / 3, (i + 1) * debugVals.length / 3).toString() + ']'], {type: "text/plain"});
            saveAs(blob, `blockRanges-${i}.json`);
        }

        var valRange = [Infinity, -Infinity];

        for (var i = 0; i < debugVals.length; ++i) {
            valRange[0] = Math.min(valRange[0], debugVals[i]);
            valRange[1] = Math.max(valRange[1], debugVals[i]);
        }
        console.log(`value range = ${valRange}`);


        dbgBuffer.unmap();
    }
    */
};

// Progressively compute the surface, returns true when rendering is complete
VolumeRaycaster.prototype.renderSurface = async function(
    isovalue, LODThreshold, viewParamUpload, renderParamsChanged, eyePos, eyeDir, upDir) {
    this.passPerfStats = {};

    if (this.renderComplete && !renderParamsChanged) {
        return this.renderComplete;
    }
    console.log("===== Rendering Surface =======");
    var startPass = performance.now();

    if (renderParamsChanged) {
        this.numVisibleBlocks = 0;
        this.newDecompressed = 0;

        this.totalPassTime = 0;
        this.numPasses = 0;
        this.renderID = Date.now().toString().slice(-6);
        this.imageCompleteness = 0;
        this.speculationCount = this.startSpecCount;
        this.speculationEnabled = this.enableSpeculationUI.checked;

        this.surfacePerfStats = [];

        this.recordVisibleBlocks = this.recordVisibleBlocksUI.checked;
        this.recordBlockActiveList = null;
        this.recordBlockVisibleList = null;
        if (this.recordVisibleBlocks) {
            console.log(
                `WARNING: Recording active/visible block statistics may effect performance!`);
            this.recordBlockActiveList = new Uint8Array(this.totalBlocks).fill(0);
            this.recordBlockVisibleList = new Uint8Array(this.totalBlocks).fill(0);
        }

        // Upload the isovalue
        await this.uploadIsovalueBuf.mapAsync(GPUMapMode.WRITE);
        new Float32Array(this.uploadIsovalueBuf.getMappedRange()).set([isovalue]);
        this.uploadIsovalueBuf.unmap();

        // Upload new LOD threshold
        var commandEncoder = this.device.createCommandEncoder();
        await this.uploadLODBuf.mapAsync(GPUMapMode.WRITE);
        new Uint32Array(this.uploadLODBuf.getMappedRange()).set([LODThreshold]);
        this.uploadLODBuf.unmap();
        commandEncoder.copyBufferToBuffer(this.uploadLODBuf, 0, this.LODThresholdBuf, 0, 4);

        // We need to reset the speculation count
        var uploadSpeculationCount = this.device.createBuffer(
            {size: 4, usage: GPUBufferUsage.COPY_SRC, mappedAtCreation: true});
        new Uint32Array(uploadSpeculationCount.getMappedRange()).set([0]);
        uploadSpeculationCount.unmap();
        commandEncoder.copyBufferToBuffer(
            uploadSpeculationCount, 0, this.viewParamBuf, (16 + 8 + 1 + 1) * 4, 4);

        // Reset active blocks for the new viewpoint/isovalue to allow eviction of old blocks
        var pass = commandEncoder.beginComputePass();
        pass.setPipeline(this.resetBlockActivePipeline);
        pass.setBindGroup(0, this.resetBlockActiveBG);
        pass.dispatchWorkgroups(Math.ceil(this.blockGridDims[0] / 8),
                                this.blockGridDims[1],
                                this.blockGridDims[2]);
        pass.end();
        this.device.queue.submit([commandEncoder.finish()]);

        var start = performance.now();
        await this.computeInitialRays(viewParamUpload);
        var end = performance.now();
        this.passPerfStats["computeInitialRays_ms"] = end - start;
        this.passPerfStats["totalBlocks"] = this.totalBlocks;
        this.passPerfStats["volumeDims"] = this.volumeDims;
        this.passPerfStats["imageSize"] = [this.width, this.height];
        this.passPerfStats["nPixels"] = this.width * this.height;
        this.passPerfStats["startSpecCount"] = this.startSpecCount;

        // Save camera info as well for reproducibility
        this.passPerfStats["eyePos"] = [eyePos[0], eyePos[1], eyePos[2]];
        this.passPerfStats["eyeDir"] = [eyeDir[0], eyeDir[1], eyeDir[2]];
        this.passPerfStats["upDir"] = [upDir[0], upDir[1], upDir[2]];

        var start = performance.now();
        await this.macroTraverse();
        var end = performance.now();
        this.passPerfStats["initialMacroTraverse_ms"] = end - start;

        start = performance.now();
        var commandEncoder = this.device.createCommandEncoder();
        var pass = commandEncoder.beginComputePass();
        pass.setPipeline(this.markRayActivePipeline);
        pass.setBindGroup(0, this.markRayActiveBG);
        pass.dispatchWorkgroups(Math.ceil(this.width / 32), this.height, 1);
        pass.end();
        // We scan the speculativeRayOffsetBuffer, so copy the ray active information over
        commandEncoder.copyBufferToBuffer(this.rayAfterActiveBuffer,
                                          0,
                                          this.speculativeRayOffsetBuffer,
                                          0,
                                          this.width * this.height * 4);
        this.device.queue.submit([commandEncoder.finish()]);
        numRaysActive = await this.scanRayAfterActive.scan(this.width * this.height);
        end = performance.now();
        this.passPerfStats["countInitialActiveRays_ms"] = end - start;
        this.passPerfStats["initialRaysActive"] = numRaysActive;
        console.log("Starting rays active: " + numRaysActive);

        var commandEncoder = this.device.createCommandEncoder();
        if (this.speculationEnabled) {
            this.speculationCount =
                Math.min(Math.floor(this.width * this.height * this.startSpecCount / numRaysActive), 64);
        } else {
            this.speculationCount = this.startSpecCount;
        }
        console.log(`First pass speculation count is ${this.speculationCount}`);
        var uploadSpeculationCount = this.device.createBuffer(
            {size: 4, usage: GPUBufferUsage.COPY_SRC, mappedAtCreation: true});
        new Uint32Array(uploadSpeculationCount.getMappedRange()).set([
            this.speculationCount
        ]);
        uploadSpeculationCount.unmap();
        commandEncoder.copyBufferToBuffer(
            uploadSpeculationCount, 0, this.viewParamBuf, (16 + 8 + 1 + 1) * 4, 4);
        this.device.queue.submit([commandEncoder.finish()]);
    }
    console.log(`++++ Surface pass ${this.numPasses} ++++`);
    this.passPerfStats["passID"] = this.numPasses;
    this.passPerfStats["speculationCount"] = this.speculationCount;
    this.passPerfStats["isovalue"] = isovalue;

    var startPass = performance.now();

    // Reset the number of blocks visible/active each pass to reduce memory usage and skip
    // computing on inactive blocks
    {
        var commandEncoder = this.device.createCommandEncoder();
        var pass = commandEncoder.beginComputePass();
        pass.setPipeline(this.resetBlockActivePipeline);
        pass.setBindGroup(0, this.resetBlockActiveBG);
        pass.dispatchWorkgroups(Math.ceil(this.blockGridDims[0] / 8),
                                this.blockGridDims[1],
                                this.blockGridDims[2]);
        pass.end();
        this.device.queue.submit([commandEncoder.finish()]);
    }

    var start = performance.now();
    await this.macroTraverse();
    var end = performance.now();
    console.log(`Macro Traverse: ${end - start}ms`);
    this.passPerfStats["macroTraverse_ms"] = end - start;

    start = performance.now();
    await this.markActiveBlocks();
    end = performance.now();
    console.log(`Mark Active Blocks: ${end - start}ms`);
    this.passPerfStats["markActiveBlocks_ms"] = end - start;

    // Decompress any new blocks needed for the pass
    start = performance.now();
    // TODO: PAss the passPerfStats through to the LRU Cache so it can write stats to it
    var [nBlocksToDecompress, decompressBlockIDs] =
        await this.lruCache.update(this.blockActiveBuffer, this.passPerfStats);
    end = performance.now();
    console.log(`LRU: ${end - start}ms`);
    this.passPerfStats["lruCacheUpdate_ms"] = end - start;
    this.passPerfStats["nBlocksToDecompress"] = nBlocksToDecompress;

    this.newDecompressed = nBlocksToDecompress;
    if (nBlocksToDecompress != 0) {
        // console.log(`Will decompress ${nBlocksToDecompress} blocks`);
        start = performance.now();
        await this.decompressBlocks(nBlocksToDecompress, decompressBlockIDs);
        end = performance.now();
        console.log(`Decompress: ${end - start}ms`);
        this.passPerfStats["decompressBlocks_ms"] = end - start;
    }

    // Now at this step we can do the block id compaction, compute # blocks visible
    var numVisibleBlocks = await this.compactVisibleBlockIDs();
    // this.numVisibleBlocks is just for displaying the value in the UI
    this.numVisibleBlocks = numVisibleBlocks;
    this.passPerfStats["nVisibleBlocks"] = numVisibleBlocks;
    var numRaysActive = 0;
    if (numVisibleBlocks > 0) {
        start = performance.now();
        // Then run a pass to write the compacted blockNumRays and blockRayOffsets instead of #
        // block buffers
        numRaysActive = await this.computeBlockRayOffsets(numVisibleBlocks);
        end = performance.now();
        this.passPerfStats["computeBlockRayOffsets_ms"] = end - start;
        this.passPerfStats["nRaysActive"] = numRaysActive;
        // console.log(`Ray active and offsets: ${end - start}ms`);
        console.log(`numRaysActive = ${numRaysActive}`);
        if (numRaysActive > 0) {
            start = performance.now();
            await this.sortActiveRaysByBlock(numRaysActive);
            end = performance.now();
            this.passPerfStats["sortActiveRaysByBlock_ms"] = end - start;
            console.log(`Sort active rays by block: ${end - start}ms`);

            start = performance.now();
            await this.raytraceVisibleBlocks(numVisibleBlocks);
            end = performance.now();
            this.passPerfStats["raytraceVisibleBlocks_ms"] = end - start;
            console.log(`Raytrace blocks: ${end - start}ms`);

            start = performance.now();
            var commandEncoder = this.device.createCommandEncoder();
            var pass = commandEncoder.beginComputePass();
            pass.setPipeline(this.depthCompositePipeline);
            pass.setBindGroup(0, this.depthCompositeBG);
            pass.setBindGroup(1, this.depthCompositeBG1);
            pass.dispatchWorkgroups(
                Math.ceil(this.width / 32), Math.ceil(this.height * this.startSpecCount / this.speculationCount), 1);
            pass.end();
            this.device.queue.submit([commandEncoder.finish()]);
            await this.device.queue.onSubmittedWorkDone();
            end = performance.now();
            this.passPerfStats["depthComposite_ms"] = end - start;

            start = performance.now();
            var commandEncoder = this.device.createCommandEncoder();
            var pass = commandEncoder.beginComputePass();
            pass.setPipeline(this.markRayActivePipeline);
            pass.setBindGroup(0, this.markRayActiveBG);
            pass.dispatchWorkgroups(Math.ceil(this.width / 32), this.height, 1);
            pass.end();
            // We scan the speculativeRayOffsetBuffer, so copy the ray active information over
            commandEncoder.copyBufferToBuffer(this.rayAfterActiveBuffer,
                                              0,
                                              this.speculativeRayOffsetBuffer,
                                              0,
                                              this.width * this.height * 4);
            this.device.queue.submit([commandEncoder.finish()]);
        
            // if (document.getElementById("outputImages").checked) {
            //     var rayAfterActiveReadback = this.device.createBuffer({
            //         size: this.width * this.height * 4,
            //         usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
            //     });
            //     var commandEncoder = this.device.createCommandEncoder();

            //     commandEncoder.copyBufferToBuffer(
            //         this.rayAfterActiveBuffer, 0, rayAfterActiveReadback, 0, rayAfterActiveReadback.size);
            //     this.device.queue.submit([commandEncoder.finish()]);
            //     await this.device.queue.onSubmittedWorkDone();
        
            //     await rayAfterActiveReadback.mapAsync(GPUMapMode.READ);
 
            //     var rayAfterActive = new Uint32Array(rayAfterActiveReadback.getMappedRange());
                
            //     var outCanvas = document.getElementById('out-canvas');
            //     var context = outCanvas.getContext('2d');
            //     var imgData = context.createImageData(outCanvas.width, outCanvas.height);
            //     for (var i = 0; i < this.width * this.height; i++) {
            //         imgData.data[i * 4] = rayAfterActive[i] * 255;
            //         imgData.data[i * 4 + 1] = rayAfterActive[i] * 255;
            //         imgData.data[i * 4 + 2] = rayAfterActive[i] * 255;
            //         imgData.data[i * 4 + 3] = 255;
            //     }
            //     context.putImageData(imgData, 0, 0);
            //     var numPasses = this.numPasses + 1;
            //     var renderID = this.renderID;
            //     outCanvas.toBlob(function(b) {
            //         saveAs(b, `${renderID}_activeMask_pass${numPasses}.png`);
            //     }, "image/png");
            //     rayAfterActiveReadback.unmap();
            //     rayAfterActiveReadback.destroy();
            // }

            numRaysActive = await this.scanRayAfterActive.scan(this.width * this.height);
            end = performance.now();
            this.passPerfStats["countRemainingActiveRays_ms"] = end - start;
            this.passPerfStats["endPassRaysActive"] = numRaysActive;

            if (document.getElementById("colorActive").checked) {
                var commandEncoder = this.device.createCommandEncoder();
                commandEncoder.copyTextureToTexture(
                    { texture: this.renderTarget}, 
                    { texture: this.renderTargetCopy},
                    { width: this.width, height: this.height, depthOrArrayLayers: 1 });
                var pass = commandEncoder.beginComputePass();
                pass.setPipeline(this.colorActiveRaysPipeline);
                pass.setBindGroup(0, this.colorActiveRaysBG);
                pass.dispatchWorkgroups(Math.ceil(this.width / 32), this.height, 1);
                pass.end();
                this.device.queue.submit([commandEncoder.finish()]);
            }

            end = performance.now();
            console.log(`PASS TOOK: ${end - startPass}ms`);
            // console.log(`num rays active after raytracing: ${numRaysActive}`);

            if (this.speculationEnabled) {
                var commandEncoder = this.device.createCommandEncoder();
                this.speculationCount =
                    Math.min(Math.floor(this.width * this.height * this.startSpecCount / numRaysActive), 64);
                // console.log(`Next pass speculation count is ${this.speculationCount}`);
                var uploadSpeculationCount = this.device.createBuffer(
                    {size: 4, usage: GPUBufferUsage.COPY_SRC, mappedAtCreation: true});
                new Uint32Array(uploadSpeculationCount.getMappedRange()).set([
                    this.speculationCount
                ]);
                uploadSpeculationCount.unmap();
                commandEncoder.copyBufferToBuffer(
                    uploadSpeculationCount, 0, this.viewParamBuf, (16 + 8 + 1 + 1) * 4, 4);
                this.device.queue.submit([commandEncoder.finish()]);
                await this.device.queue.onSubmittedWorkDone();
                console.log(`End pass rays active: ${numRaysActive}`);
                console.log(`Next pass speculation count: ${this.speculationCount}`);     
            }
            console.log(`++++++++++`);
        }
    } else {
        this.passPerfStats["nRaysActive"] = numRaysActive;
        this.passPerfStats["endPassRaysActive"] = numRaysActive;
    }
    var endPass = performance.now();
    this.passPerfStats["totalPassTime_ms"] = endPass - startPass;
    this.passPerfStats["memory"] = this.getMemoryStats();
    this.surfacePerfStats.push(this.passPerfStats);

    console.log("=============");
    this.totalPassTime += endPass - startPass;
    this.numPasses += 1;
    //}
    this.renderComplete = numRaysActive == 0;
    this.imageCompleteness = (this.width * this.height - numRaysActive) / (this.width * this.height);
    this.passPerfStats["imageCompleteness"] = this.imageCompleteness;
    this.passPerfStats["imageCompletenessThreshold"] = parseFloat(document.getElementById("completenessThreshold").value);
    if ( this.imageCompleteness >= parseFloat(document.getElementById("completenessThreshold").value)) { 
        console.log(`Render complete with image completeness: ${this.imageCompleteness}`)
        this.renderComplete = true;        
    }
    if (this.renderComplete) {
        if (this.recordVisibleBlocks) {
            var nTotalBlocksActive = 0;
            var nTotalBlocksVisible = 0;
            for (var i = 0; i < this.totalBlocks; ++i) {
                if (this.recordBlockActiveList[i] != 0) {
                    ++nTotalBlocksActive;
                }
                if (this.recordBlockVisibleList[i] != 0) {
                    ++nTotalBlocksVisible;
                }
            }
            console.log(`Total blocks active: ${nTotalBlocksActive} (${
                nTotalBlocksActive / this.totalBlocks * 100})`);
            console.log(`Total blocks visible: ${nTotalBlocksVisible} (${
                nTotalBlocksVisible / this.totalBlocks * 100})`);
            this.passPerfStats["nTotalBlocksActive"] = nTotalBlocksActive;
            this.passPerfStats["nTotalBlocksVisible"] = nTotalBlocksVisible;
        }
        console.log(`Avg time per pass ${this.totalPassTime / this.numPasses}ms`);
        console.log(this.passPerfStats);
    }
    return this.renderComplete;
};

// Reset the rays and compute the initial set of rays that intersect the volume
// Rays that miss the volume will have:
// dir = vec3(0)
// block_id = UINT_MAX
// t = FLT_MAX
//
// Rays that hit the volume will have valid directions and t values
VolumeRaycaster.prototype.computeInitialRays = async function(viewParamUpload) {
    var commandEncoder = this.device.createCommandEncoder();

    commandEncoder.copyBufferToBuffer(
        viewParamUpload, 0, this.viewParamBuf, 0, (16 + 8 + 1) * 4);

    var resetRaysPass = commandEncoder.beginComputePass(this.resetRaysPipeline);
    resetRaysPass.setBindGroup(0, this.resetRaysBG);
    resetRaysPass.setPipeline(this.resetRaysPipeline);
    resetRaysPass.dispatchWorkgroups(Math.ceil(this.width / 8), this.height, 1);
    resetRaysPass.end();

    var initialRaysPass = commandEncoder.beginRenderPass(this.initialRaysPassDesc);

    initialRaysPass.setPipeline(this.initialRaysPipeline);
    initialRaysPass.setVertexBuffer(0, this.dataBuf);
    initialRaysPass.setBindGroup(0, this.initialRaysBindGroup);
    initialRaysPass.draw(12 * 3, 1, 0, 0);

    initialRaysPass.end();
    this.device.queue.submit([commandEncoder.finish()]);
    await this.device.queue.onSubmittedWorkDone();
};

// Step the active rays forward in the macrocell grid, updating block_id and t.
// Rays that exit the volume will have block_id = UINT_MAX and t = FLT_MAX
VolumeRaycaster.prototype.macroTraverse = async function() {
    // TODO: Would it be worth doing a scan and compact to find just the IDs of the currently
    // active rays? then only advancing them? We'll do that anyways so it would tell us when
    // we're done too (no rays active)
    var commandEncoder = this.device.createCommandEncoder();

    // Reset speculative IDs buffer (here for now but could be moved)
    var resetSpecIDsPass = commandEncoder.beginComputePass();
    resetSpecIDsPass.setBindGroup(0, this.resetSpeculativeIDsBG);
    resetSpecIDsPass.setPipeline(this.resetSpeculativeIDsPipeline);
    resetSpecIDsPass.dispatchWorkgroups(Math.ceil(this.width / 32), this.height * this.startSpecCount, 1);
    resetSpecIDsPass.end();

    // Update the current pass index
    var uploadPassIndex = this.device.createBuffer(
        {size: 4, usage: GPUBufferUsage.COPY_SRC, mappedAtCreation: true});
    new Int32Array(uploadPassIndex.getMappedRange()).set([this.numPasses]);
    uploadPassIndex.unmap();

    commandEncoder.copyBufferToBuffer(this.uploadIsovalueBuf, 0, this.volumeInfoBuffer, 52, 4);
    commandEncoder.copyBufferToBuffer(
        uploadPassIndex, 0, this.viewParamBuf, (16 + 8 + 1) * 4, 4);

    var pass = commandEncoder.beginComputePass();

    pass.setPipeline(this.macroTraversePipeline);
    pass.setBindGroup(0, this.macroTraverseBindGroup);
    pass.setBindGroup(1, this.macroTraverseRangesBG);
    pass.dispatchWorkgroups(Math.ceil(this.width / 64), this.height, 1);

    pass.end();
    this.device.queue.submit([commandEncoder.finish()]);
    await this.device.queue.onSubmittedWorkDone();

    // Log speculative ray IDs buffer
    // var readbackSpeculativeIDBuffer = this.device.createBuffer({
    //     size: this.speculativeRayIDBuffer.size,
    //     usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
    // });
    // var commandEncoder = this.device.createCommandEncoder();
    // commandEncoder.copyBufferToBuffer(
    //     this.speculativeRayIDBuffer, 0, readbackSpeculativeIDBuffer, 0,
    //     this.speculativeRayIDBuffer.size);
    // this.device.queue.submit([commandEncoder.finish()]);
    // await this.device.queue.onSubmittedWorkDone();
    // await readbackSpeculativeIDBuffer.mapAsync(GPUMapMode.READ);
    // var specIDs = new Uint32Array(readbackSpeculativeIDBuffer.getMappedRange());
    // console.log(specIDs);

    uploadPassIndex.destroy();
};

// Mark the active and visible blocks for the current viewpoint/isovalue and count the # of
// rays that we need to process for each block
VolumeRaycaster.prototype.markActiveBlocks = async function() {
    var commandEncoder = this.device.createCommandEncoder();
    var pass = commandEncoder.beginComputePass();

    // Compute which blocks are active and how many rays each has
    pass.setPipeline(this.markBlockActivePipeline);
    pass.setBindGroup(0, this.markBlockActiveBG);
    pass.setBindGroup(1, this.renderTargetDebugBG);
    pass.dispatchWorkgroups(Math.ceil(this.width / 32), this.height * this.startSpecCount, 1);

    pass.end();

    this.device.queue.submit([commandEncoder.finish()]);
    await this.device.queue.onSubmittedWorkDone();

    if (this.recordVisibleBlocks) {
        var activeReadback = this.device.createBuffer({
            size: 4 * this.totalBlocks,
            usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
        });
        var visibleReadback = this.device.createBuffer({
            size: 4 * this.totalBlocks,
            usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
        });

        var commandEncoder = this.device.createCommandEncoder();
        commandEncoder.copyBufferToBuffer(
            this.blockActiveBuffer, 0, activeReadback, 0, activeReadback.size);
        commandEncoder.copyBufferToBuffer(
            this.blockVisibleBuffer, 0, visibleReadback, 0, visibleReadback.size);
        this.device.queue.submit([commandEncoder.finish()]);
        await this.device.queue.onSubmittedWorkDone();

        await activeReadback.mapAsync(GPUMapMode.READ);
        await visibleReadback.mapAsync(GPUMapMode.READ);

        var nActiveBlocks = 0;

        var blockActive = new Uint32Array(activeReadback.getMappedRange());
        var blockVisible = new Uint32Array(visibleReadback.getMappedRange());
        for (var i = 0; i < this.totalBlocks; ++i) {
            if (blockActive[i]) {
                ++nActiveBlocks;
            }
            this.recordBlockActiveList[i] |= blockActive[i];
            this.recordBlockVisibleList[i] |= blockVisible[i];
        }
        this.passPerfStats["nActiveBlocks"] = nActiveBlocks;

        activeReadback.unmap();
        visibleReadback.unmap();

        activeReadback.destroy();
        visibleReadback.destroy();
    }
};

// Compute the number of visible blocks and compact their IDs. Also produces an offset buffer
// mapping from original block index -> compacted location
// - visibleBlockIDBuffer: compact list of active block IDs (visible block IDs)
// - blockVisibleCompactOffsets: mapping from block ID to compacted location
VolumeRaycaster.prototype.compactVisibleBlockIDs = async function() {
    var start = performance.now();
    // Populate the ray ID, ray block ID and ray active buffers
    var commandEncoder = this.device.createCommandEncoder();

    // We scan the active block buffer to produce offsets for compacting active block IDs
    // down This will let us reduce the dispatch size of the ray tracing step to just active
    // blocks
    commandEncoder.copyBufferToBuffer(this.blockVisibleBuffer,
                                      0,
                                      this.blockVisibleCompactOffsetBuffer,
                                      0,
                                      this.totalBlocks * 4);
    this.device.queue.submit([commandEncoder.finish()]);

    // Compact the visible block IDs down
    var numVisibleBlocks = await this.scanBlockVisibleOffsets.scan(this.totalBlocks);
    if (numVisibleBlocks === 0) {
        return numVisibleBlocks;
    }

    this.visibleBlockIDBuffer = this.device.createBuffer({
        size: 4 * numVisibleBlocks,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
    });

    await this.streamCompact.compactActiveIDs(this.totalBlocks,
                                              this.blockVisibleBuffer,
                                              this.blockVisibleCompactOffsetBuffer,
                                              this.visibleBlockIDBuffer);
    var end = performance.now();
    console.log(`compactVisibleBlockIDs: ${end - start}ms`);

    return numVisibleBlocks;
};

// Compute the number of visible rays per block, scan the blockNumRaysBuffer storing the output
// in blockRayOffsetBuffer and return the number of active rays.
VolumeRaycaster.prototype.computeBlockRayOffsets = async function(numVisibleBlocks) {
    this.blockNumRaysBuffer = this.device.createBuffer({
        size: 4 * numVisibleBlocks,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
    });

    // Scan result buffer for the block ray offsets (computed by scanning the result in
    // blockNumRaysBuffer)
    this.blockRayOffsetBuffer = this.device.createBuffer({
        size: 4 * this.scanPipeline.getAlignedSize(numVisibleBlocks),
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
    });
    this.scanBlockRayOffsets = this.scanPipeline.prepareGPUInput(
        this.blockRayOffsetBuffer, this.scanPipeline.getAlignedSize(numVisibleBlocks));

    var countBlockRaysBG = this.device.createBindGroup({
        layout: this.countBlockRaysBGLayout,
        entries: [
            {binding: 0, resource: {buffer: this.volumeInfoBuffer}},
            {binding: 1, resource: {buffer: this.blockNumRaysBuffer}},
            {binding: 2, resource: {buffer: this.rayBlockIDBuffer}},
            {binding: 3, resource: {buffer: this.blockVisibleCompactOffsetBuffer}},
        ]
    });

    // Now here we can run a compute pass to compute # of rays per block, this is image size
    // dispatch so no need to chunk it
    var commandEncoder = this.device.createCommandEncoder();
    var pass = commandEncoder.beginComputePass();
    pass.setPipeline(this.countBlockRaysPipeline);
    pass.setBindGroup(0, countBlockRaysBG);
    pass.dispatchWorkgroups(Math.ceil(this.width / 32), this.height * this.startSpecCount, 1);
    pass.end();

    commandEncoder.copyBufferToBuffer(this.blockNumRaysBuffer,
                                      0,
                                      this.blockRayOffsetBuffer,
                                      0,
                                      this.blockNumRaysBuffer.size);
    await this.device.queue.submit([commandEncoder.finish()]);

    return await this.scanBlockRayOffsets.scan(numVisibleBlocks);
};

// Sort the active ray IDs by their block ID in ascending order (inactive rays will be at the
// end).
VolumeRaycaster.prototype.sortActiveRaysByBlock = async function(numRaysActive) {
    // Populate the ray ID, ray block ID and ray active buffers
    var commandEncoder = this.device.createCommandEncoder();
    var pass = commandEncoder.beginComputePass()
    pass.setPipeline(this.writeRayAndBlockIDPipeline);
    pass.setBindGroup(0, this.writeRayAndBlockIDBG);
    pass.dispatchWorkgroups(Math.ceil(this.width / 8), this.height * this.startSpecCount, 1);
    pass.end();

    // We scan the rayActiveCompactOffsetBuffer, so copy the ray active information over
    commandEncoder.copyBufferToBuffer(this.rayActiveBuffer,
                                      0,
                                      this.rayActiveCompactOffsetBuffer,
                                      0,
                                      this.width * this.height * this.startSpecCount * 4);

    this.device.queue.submit([commandEncoder.finish()]);

    // Scan the active ray buffer and compact the active ray IDs before we sort
    // so that the sort doesn't have to process such a large number of items
    // TODO: This is not matching numRaysActive?
    var nactive = await this.scanRayActive.scan(this.width * this.height * this.startSpecCount);
    // Should match numRaysActive, sanity check here
    if (numRaysActive != nactive) {
        console.log(`nactive ${nactive} doesn't match numRaysActive ${numRaysActive}!?`);
    }
    var startCompacts = performance.now();
    // Compact the active ray IDs and their block IDs down
    await this.streamCompact.compactActive(this.width * this.height * this.startSpecCount,
                                           this.rayActiveBuffer,
                                           this.rayActiveCompactOffsetBuffer,
                                           this.speculativeRayIDBuffer,
                                           this.rayIDBuffer);

    await this.streamCompact.compactActiveIDs(this.width * this.height * this.startSpecCount,
                                              this.rayActiveBuffer,
                                              this.rayActiveCompactOffsetBuffer,
                                              this.compactSpeculativeIDBuffer);

    await this.streamCompact.compactActive(this.width * this.height * this.startSpecCount,
                                           this.rayActiveBuffer,
                                           this.rayActiveCompactOffsetBuffer,
                                           this.rayBlockIDBuffer,
                                           this.compactRayBlockIDBuffer);

    var endCompacts = performance.now();
    console.log(`sortActiveRaysByBlock: Compacts ${endCompacts - startCompacts}ms`);

    var compactRayBlockIDBufferCopy = this.device.createBuffer({
        size: this.radixSorter.getAlignedSize(this.compactRayBlockIDBuffer.size / 4) * 4,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
    });
    var commandEncoder = this.device.createCommandEncoder();
    commandEncoder.copyBufferToBuffer(this.compactRayBlockIDBuffer,
                                      0,
                                      compactRayBlockIDBufferCopy,
                                      0,
                                      this.compactRayBlockIDBuffer.size);
    this.device.queue.submit([commandEncoder.finish()]);
    await this.device.queue.onSubmittedWorkDone();

    var start = performance.now();
    // Sort active ray IDs by their block ID
    await this.radixSorter.sort(
        this.compactRayBlockIDBuffer, this.rayIDBuffer, numRaysActive, false);
    await this.radixSorter.sort(
        compactRayBlockIDBufferCopy, this.compactSpeculativeIDBuffer, numRaysActive, false);
    var end = performance.now();
    console.log(`sortActiveRaysByBlock: Sort rays by blocks: ${end - start}ms`);
};

VolumeRaycaster.prototype.raytraceVisibleBlocks = async function(numVisibleBlocks) {
    /*
    console.log(
        `Raytracing ${numVisibleBlocks} blocks, speculation count = ${this.speculationCount}`);
        */

    // Must recreate each time b/c cache buffer will grow
    var rtBlocksPipelineBG0 = this.device.createBindGroup({
        layout: this.rtBlocksPipelineBG0Layout,
        entries: [
            {binding: 0, resource: {buffer: this.volumeInfoBuffer}},
            {binding: 1, resource: {buffer: this.lruCache.cache}},
            {binding: 2, resource: {buffer: this.lruCache.cachedItemSlots}},
        ]
    });

    this.combinedBlockInformationBuffer = this.device.createBuffer(
        {size: numVisibleBlocks * 4 * 4, usage: GPUBufferUsage.STORAGE});

    this.rtBlocksPipelineBG1 = this.device.createBindGroup({
        layout: this.rtBlocksPipelineBG1Layout,
        entries: [
            {binding: 0, resource: {buffer: this.viewParamBuf}},
            {binding: 1, resource: {buffer: this.rayInformationBuffer}},
            {binding: 2, resource: {buffer: this.rayIDBuffer}},
            {binding: 3, resource: {buffer: this.combinedBlockInformationBuffer}},
            {binding: 4, resource: this.renderTarget.createView()},
            {binding: 5, resource: {buffer: this.compactSpeculativeIDBuffer}},
            {binding: 6, resource: {buffer: this.rayRGBZBuffer}},
        ]
    });

    this.combineBlockInformationBG = this.device.createBindGroup({
        layout: this.combineBlockInformationBGLayout,
        entries: [
            {binding: 0, resource: {buffer: this.combinedBlockInformationBuffer}},
            {binding: 1, resource: {buffer: this.visibleBlockIDBuffer}},
            {binding: 2, resource: {buffer: this.blockRayOffsetBuffer}},
            {binding: 3, resource: {buffer: this.blockNumRaysBuffer}},
            {binding: 4, resource: {buffer: this.blockActiveBuffer}},
        ]
    });

    var commandEncoder = this.device.createCommandEncoder();
    {
        const groupThreadCount = 64;
        const totalWorkGroups = Math.ceil(numVisibleBlocks / groupThreadCount);

        var pushConstants = buildPushConstantsBuffer(
            this.device, totalWorkGroups, new Uint32Array([numVisibleBlocks]));

        var blockIDOffsetBG = this.device.createBindGroup({
            layout: this.pushConstantS1B0DynamicLayout,
            entries: [{binding: 0, resource: {buffer: pushConstants.gpuBuffer, size: 12}}]
        });

        var pass = commandEncoder.beginComputePass();
        // First make the combined block information buffer (to fit in 6 storage buffers)
        // The limit will be bumped up to 8 so we could remove this piece in a bit once
        // the change lands in Chromium
        pass.setPipeline(this.combineBlockInformationPipeline);
        pass.setBindGroup(0, this.combineBlockInformationBG);
        for (var i = 0; i < pushConstants.nOffsets; ++i) {
            pass.setBindGroup(1, blockIDOffsetBG, pushConstants.dynamicOffsets, i, 1);
            pass.dispatchWorkgroups(pushConstants.dispatchSizes[i], 1, 1);
        }
        pass.end();
    }

    {
        var pushConstants = buildPushConstantsBuffer(this.device, numVisibleBlocks);

        var blockIDOffsetBG = this.device.createBindGroup({
            layout: this.pushConstantS1B0DynamicLayout,
            entries: [{binding: 0, resource: {buffer: pushConstants.gpuBuffer, size: 8}}]
        });

        var pass = commandEncoder.beginComputePass();
        // TODO: Might be worth for data sets where many blocks
        // project to a lot of pixels to split up the dispatches,
        // and do multiple dispatch indirect for the blocks touching
        // many pixels so that we don't serialize so badly on them, while
        // still doing a single dispatch for all blocks touching <= 64 pixels
        // since that will be most of the blocks, especially for large data.
        pass.setPipeline(this.raytraceBlocksPipeline);
        pass.setBindGroup(0, rtBlocksPipelineBG0);
        pass.setBindGroup(1, this.rtBlocksPipelineBG1);
        for (var i = 0; i < pushConstants.nOffsets; ++i) {
            pass.setBindGroup(2, blockIDOffsetBG, pushConstants.dynamicOffsets, i, 1);
            pass.dispatchWorkgroups(pushConstants.dispatchSizes[i], 1, 1);
        }
        pass.end();
    }

    this.device.queue.submit([commandEncoder.finish()]);
    await this.device.queue.onSubmittedWorkDone();
};

VolumeRaycaster.prototype.decompressBlocks =
    async function(nBlocksToDecompress, decompressBlockIDs) {
    var decompressBlocksBG = this.device.createBindGroup({
        layout: this.decompressBlocksBGLayout,
        entries: [
            {
                binding: 0,
                resource: {
                    buffer: this.compressedBuffer,
                },
            },
            {
                binding: 1,
                resource: {
                    buffer: this.volumeInfoBuffer,
                },
            },
            {
                binding: 2,
                resource: {
                    buffer: this.lruCache.cache,
                },
            },
            {
                binding: 3,
                resource: {
                    buffer: decompressBlockIDs,
                },
            },
            {
                binding: 4,
                resource: {
                    buffer: this.lruCache.cachedItemSlots,
                },
            },
        ],
    });

    var workGroupCount = Math.ceil(nBlocksToDecompress / 64.0);
    var numChunks = Math.ceil(workGroupCount / this.maxDispatchSize);
    var dispatchChunkOffsetsBuf = this.device.createBuffer({
        size: numChunks * 256,
        usage: GPUBufferUsage.UNIFORM,
        mappedAtCreation: true,
    });
    var map = new Uint32Array(dispatchChunkOffsetsBuf.getMappedRange());
    for (var i = 0; i < numChunks; ++i) {
        map[i * 64] = i * this.maxDispatchSize;
        map[i * 64 + 1] = nBlocksToDecompress;
    }
    dispatchChunkOffsetsBuf.unmap();

    // We execute these chunks in separate submissions to avoid having them
    // execute all at once and trigger a TDR if we're decompressing a large amount of data
    for (var i = 0; i < numChunks; ++i) {
        var numWorkGroups =
            Math.min(workGroupCount - i * this.maxDispatchSize, this.maxDispatchSize);
        var commandEncoder = this.device.createCommandEncoder();
        var pass = commandEncoder.beginComputePass();
        pass.setPipeline(this.decompressBlocksPipeline);
        pass.setBindGroup(0, decompressBlocksBG);
        // Have to create bind group here because dynamic offsets are not allowed
        var decompressBlocksStartOffsetBG = this.device.createBindGroup({
            layout: this.ub1binding0BGLayout,
            entries: [
                {
                    binding: 0,
                    resource: {
                        buffer: dispatchChunkOffsetsBuf,
                        size: 8,
                        offset: i * 256,
                    },
                },
            ],
        });
        pass.setBindGroup(1, decompressBlocksStartOffsetBG);
        pass.dispatchWorkgroups(Math.ceil(numWorkGroups), 1, 1);
        pass.end();
        this.device.queue.submit([commandEncoder.finish()]);
    }
    await this.device.queue.onSubmittedWorkDone();
    dispatchChunkOffsetsBuf.destroy();
};

back to top

Software Heritage — Copyright (C) 2015–2026, The Software Heritage developers. License: GNU AGPLv3+.
The source code of Software Heritage itself is available on our development forge.
The source code files archived by Software Heritage are available under their own copyright and licenses.
Terms of use: Archive access, API— Content policy— Contact— JavaScript license information— Web API