diff --git a/Test/Interactive/rtbCompareManyRenderings.m b/Test/Interactive/rtbCompareManyRenderings.m
new file mode 100644
index 0000000000000000000000000000000000000000..205202249aff76603eb47afcde5d843e99fe4025
--- /dev/null
+++ b/Test/Interactive/rtbCompareManyRenderings.m
@@ -0,0 +1,111 @@
+function [comparisons, matchInfo] = rtbCompareManyRenderings(folderA, folderB, varargin)
+%% Compare paris of renderings across two folders.
+%
+% comparisons = rtbCompareManyRenderings(folderA, folderB) finds rendering
+% data files in the given folderA and folderB and attempts to match up
+% pairs of renderings that came from the same recipe and renderer.
+% For each pair, computes difference images and statistics.
+%
+% Returns a struct array of image comparisons, as returned from
+% rtbCompareRenderings().
+%
+% rtbCompareManyRenderings( ... 'fetchReferenceData', fetchReferenceData)
+% specify whether to use Remote Data Toolbox to fetch reference data for
+% comparison.  The default is true, fetch reference data when there is a
+% recipe in folderA that was not found in folderB, and cache the fetched
+% data in folderB.
+%
+%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team.
+%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us
+%%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
+
+parser = inputParser();
+parser.KeepUnmatched = true;
+parser.addRequired('folderA', @ischar);
+parser.addRequired('folderB', @ischar);
+parser.addParameter('fetchReferenceData', true, @islogical);
+parser.parse(folderA, folderB, varargin{:});
+folderA = parser.Results.folderA;
+folderB = parser.Results.folderB;
+fetchReferenceData = parser.Results.fetchReferenceData;
+
+
+%% Identify renderings and recipes to compare.
+renderingsA = rtbFindRenderings(folderA, varargin{:});
+recipeNames = unique({renderingsA.recipeName});
+nRecipes = numel(recipeNames);
+
+renderingsB = rtbFindRenderings(folderB, varargin{:});
+
+%% Compare one recipe at a time, fetch data as necessary.
+comparisonsCell = cell(1, nRecipes);
+matchInfoCell = cell(1, nRecipes);
+for rr = 1:nRecipes
+    recipeName = recipeNames{rr};
+    fprintf('Comparing renderings for recipe <%s>.\n', recipeName);
+    
+    isRecipeA = strcmp({renderingsA.recipeName}, recipeName);
+    recipeRenderingsA = renderingsA(isRecipeA);
+    
+    % fetch missing recipe for B?
+    isRecipeB = strcmp({renderingsB.recipeName}, recipeName);
+    if any(isRecipeB)
+        recipeRenderingsB = renderingsB(isRecipeB);
+    elseif fetchReferenceData
+        recipeRenderingsB = rtbFetchReferenceData(recipeName, varargin{:});
+        if isempty(recipeRenderingsB)
+            fprintf('  Could not fetch reference data for set B, skipping this recipe.\n');
+            continue;
+        end
+    else
+        fprintf('  Could not find local data for set B.  Skipping this recipe.\n');
+        continue;
+    end
+    
+    % match pairs of renderings for recipes A and B
+    info = matchRenderingPairs(recipeRenderingsA, recipeRenderingsB);
+    fprintf('  Found %d matched pairs of renderings.\n', info.nPairs);
+    
+    % run a comparison for each matched pair
+    pairsCell = cell(1, info.nPairs);
+    for pp = 1:info.nPairs
+        fprintf('    %s.\n', info.matchedA(pp).identifier);
+        pairsCell{pp} = rtbCompareRenderings(info.matchedA(pp), info.matchedB(pp), varargin{:});
+    end
+    comparisonsCell{rr} = [pairsCell{:}];
+    matchInfoCell{rr} = info;
+    
+    % report on unmatched renderings
+    if ~isempty(info.unmatchedA)
+        nUnmatched = info.unmatchedA;
+        fprintf('  %d renderings in A were not matched in B:\n', nUnmatched);
+        for uu = 1:nUnmatched
+            fprintf('    %s\n', info.info.unmatchedA(uu).identifier);
+        end
+    end
+    
+    if ~isempty(info.unmatchedB)
+        nUnmatched = info.unmatchedB;
+        fprintf('  %d renderings in B were not matched in A:\n', nUnmatched);
+        for uu = 1:nUnmatched
+            fprintf('    %s\n', info.info.unmatchedB(uu).identifier);
+        end
+    end
+end
+comparisons = [comparisonsCell{:}];
+matchInfo = [matchInfoCell{:}];
+
+
+%% For pairs of comparable renderings from two sets.
+function info = matchRenderingPairs(renderingsA, renderingsB)
+identifiersA = {renderingsA.identifier};
+identifiersB = {renderingsB.identifier};
+[~, indexA, indexB] = intersect(identifiersA, identifiersB, 'stable');
+[~, unmatchedIndexA] = setdiff(identifiersA, identifiersB);
+[~, unmatchedIndexB] = setdiff(identifiersB, identifiersA);
+
+info.nPairs = numel(indexA);
+info.matchedA = renderingsA(indexA);
+info.matchedB = renderingsB(indexB);
+info.unmatchedA = renderingsA(unmatchedIndexA);
+info.unmatchedB = renderingsB(unmatchedIndexB);
diff --git a/Test/Interactive/rtbCompareRenderings.m b/Test/Interactive/rtbCompareRenderings.m
index d9e7a283aab643b6151eba4cb74ef292b1311245..471a7fb162968b50f436c3c8ab1f6970ca81ebe9 100644
--- a/Test/Interactive/rtbCompareRenderings.m
+++ b/Test/Interactive/rtbCompareRenderings.m
@@ -28,6 +28,7 @@ function comparison = rtbCompareRenderings(renderingA, renderingB, varargin)
 %%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
 
 parser = inputParser();
+parser.KeepUnmatched = true;
 parser.addRequired('renderingA', @isstruct);
 parser.addRequired('renderingB', @isstruct);
 parser.addParameter('denominatorThreshold', 0.2, @isnumeric);
diff --git a/Test/Interactive/rtbFetchReferenceData.m b/Test/Interactive/rtbFetchReferenceData.m
index 8b400f66c862002c233f8a601277efbe7a9f58aa..adca9695739b4a1568e18a11029764114c8b1172 100644
--- a/Test/Interactive/rtbFetchReferenceData.m
+++ b/Test/Interactive/rtbFetchReferenceData.m
@@ -30,6 +30,7 @@ function [renderings, referenceRoot, artifact] = rtbFetchReferenceData(recipeNam
 %%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
 
 parser = inputParser();
+parser.KeepUnmatched = true;
 parser.addRequired('recipeName', @ischar);
 parser.addParameter('rdtConfig', 'render-toolbox');
 parser.addParameter('remotePath', 'reference-data', @ischar);
@@ -42,12 +43,17 @@ remotePath = parser.Results.remotePath;
 referenceVersion = parser.Results.referenceVersion;
 referenceRoot = parser.Results.referenceRoot;
 
-
 %% Get a whole recipe from the server.
 artifactPath = fullfile(remotePath, recipeName);
-[fileName, artifact] = rdtReadArtifact(rdtConfig, artifactPath, recipeName, ...
-    'version', referenceVersion, ...
-    'type', 'zip');
+try
+    [fileName, artifact] = rdtReadArtifact(rdtConfig, artifactPath, recipeName, ...
+        'version', referenceVersion, ...
+        'type', 'zip');
+catch err
+    renderings = [];
+    artifact = [];
+    return;
+end
 
 if isempty(fileName)
     renderings = [];
@@ -58,6 +64,9 @@ end
 
 %% Explode renderings it into the destination folder.
 destination = fullfile(referenceRoot, recipeName);
+if 7 ~= exist(destination, 'dir')
+    mkdir(destination);
+end
 unzip(fileName, destination);
 
 % scan for rendering records
diff --git a/Test/Interactive/rtbFindRenderings.m b/Test/Interactive/rtbFindRenderings.m
index cdde19cb50fced1a4ba722f4c9b6fd6ca2b2adaa..d6b7b2285111b338df45ead07be55d9e3fbaed77 100644
--- a/Test/Interactive/rtbFindRenderings.m
+++ b/Test/Interactive/rtbFindRenderings.m
@@ -32,6 +32,7 @@ function renderings = rtbFindRenderings(rootFolder, varargin)
 %%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
 
 parser = inputParser();
+parser.KeepUnmatched = true;
 parser.addRequired('rootFolder', @ischar);
 parser.addParameter('filter', '\.mat$', @ischar);
 parser.addParameter('renderingsFolderName', 'renderings', @ischar);
diff --git a/Test/Interactive/rtbPlotRenderingComparison.m b/Test/Interactive/rtbPlotRenderingComparison.m
index 60a89741bc038b7f56b1660f3b661ac8f2983682..b5526fae0c212a393a8980d9059ae6ab20ab3e2a 100644
--- a/Test/Interactive/rtbPlotRenderingComparison.m
+++ b/Test/Interactive/rtbPlotRenderingComparison.m
@@ -10,6 +10,7 @@ function fig = rtbPlotRenderingComparison(comparison, varargin)
 %%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
 
 parser = inputParser();
+parser.KeepUnmatched = true;
 parser.addRequired('comparison', @isstruct);
 parser.addParameter('isScale', true, @islogical);
 parser.addParameter('toneMapFactor', 0, @isnumeric);
@@ -34,7 +35,10 @@ rgbBminusA = rtbMultispectralToSRGB(comparison.bMinusA, S, ...
 
 
 %% Make the plot.
-name = sprintf('RGB isScale %d toneMapFactor %.2f', isScale, toneMapFactor);
+name = sprintf('%s isScale %d toneMapFactor %.2f', ...
+    comparison.renderingA.identifier, ...
+    isScale, ...
+    toneMapFactor);
 set(fig, 'Name', name, ...
     'NumberTitle', 'off');