static float EncodingToSlope(int encoding) => 4f * encoding / 1000000f;  // Haven't looked at the numerics closely to see whether these are exact inverses.  Don't care enough yet.

        /// <summary>
        /// Generate the near horizon for the patch, overwriting what was there.
        /// The horizons will be in slope format, not angles
        /// </summary>
        /// <param name="target"></param>
        public void AddNearHorizon(TerrainPatch target)
        {
            Debug.Assert(Terrain != null);
            Debug.Assert(target != null);
            if (target.Horizons == null)
            {
                target.InitializeHorizons();
            }
            using (var context = new Context())
            {
                AcceleratorId aid = Accelerator.Accelerators.Where(id => id.AcceleratorType == AcceleratorType.Cuda).FirstOrDefault();
                //AcceleratorId aid = Accelerator.Accelerators.Where(id => id.AcceleratorType == AcceleratorType.CPU).FirstOrDefault();
                if (aid.AcceleratorType != AcceleratorType.Cuda)
                {
                    Console.WriteLine(@"There is no CUDA accelerator present.  Doing nothing.");
                    return;
                }
                using (var accelerator = Accelerator.Create(context, aid))
                {
                    target.Matrices = null;  // Be sure!!
                    target.FillPoints(Terrain);
                    target.FillMatricesRelativeToPoint(Terrain, target.Points[0][0]);

                    // Matrices
                    var cpu_matrices_size = target.Height * target.Width * 12;
                    var basePoint         = target.Points[0][0];
                    var cpu_matrices      = MakeCPUMatrices(target);

                    // Horizon (load from target)
                    var cpu_horizon_size = target.Height * target.Width * Horizon.HorizonSamples;
                    var cpu_horizon      = new float[cpu_horizon_size];

                    // Initialize to float.MinValue.  Maintain horizon as slope
                    for (var i = 0; i < cpu_horizon_size; i++)
                    {
                        cpu_horizon[i] = float.MinValue;
                    }

                    // Caster array
                    const int dem_size    = TerrainPatch.DEM_size;
                    const int patch_size  = TerrainPatch.DefaultSize;
                    var       maxDistance = (float)TerrainPatch.MaximumLocalDistance;
                    var       border      = 2 + (int)Math.Ceiling(maxDistance); // the 2 is margin for the bilinear interpolation

                    var line_min    = Math.Max(0, target.Line - border);
                    var line_max    = Math.Min(dem_size - 1, target.Line + patch_size + border); // 1 more than the highest index
                    var line_size   = line_max - line_min;
                    var line_offset = target.Line - line_min;

                    var sample_min    = Math.Max(0, target.Sample - border);
                    var sample_max    = Math.Min(dem_size - 1, target.Sample + patch_size + border); // 1 more than the highest index
                    var sample_size   = sample_max - sample_min;
                    var sample_offset = target.Sample - sample_min;

                    var cpu_caster_points_size = line_size * sample_size * 3;
                    var cpu_caster_points      = new float[cpu_caster_points_size];
                    FillNearCasterArray(cpu_caster_points, target.Points[0][0], line_min, line_max, sample_min, sample_max);

                    //DumpNearFieldTestCsv(cpu_caster_points, cpu_matrices);

                    var test_floats_size = Horizon.HorizonSamples * TerrainPatch.NearHorizonOversample;
                    var test_floats      = new float[test_floats_size];

                    using (var gpu_matrices = accelerator.Allocate <float>(cpu_matrices_size))
                        using (var gpu_horizon = accelerator.Allocate <float>(cpu_horizon_size))
                            using (var gpu_caster_points = accelerator.Allocate <float>(cpu_caster_points_size))
                                using (var gpu_test_floats = accelerator.Allocate <float>(test_floats_size))
                                {
                                    gpu_matrices.CopyFrom(cpu_matrices, 0, 0, cpu_matrices_size);
                                    gpu_horizon.CopyFrom(cpu_horizon, 0, 0, cpu_horizon_size);
                                    gpu_caster_points.CopyFrom(cpu_caster_points, 0, 0, cpu_caster_points_size);
                                    gpu_test_floats.CopyFrom(test_floats, 0, 0, test_floats_size);

                                    var   groupSize       = accelerator.MaxNumThreadsPerGroup;
                                    Index launchDimension = Horizon.HorizonSamples * TerrainPatch.NearHorizonOversample;

                                    const float d_min  = 1f;
                                    var         d_max  = (float)TerrainPatch.MaximumLocalDistance;
                                    const float d_step = TerrainPatch.LocalStep;

                                    var kernel1 = accelerator.LoadAutoGroupedStreamKernel <Index, int, int, int, int, int, int, float, float, float, ArrayView <float>, ArrayView <float>, ArrayView <float>, ArrayView <float> >(NearHorizonKernel1);

                                    Console.WriteLine(@"Launching near horizon kernels ... ");
                                    if (true)
                                    {
                                        for (var target_line = 0; target_line < TerrainPatch.DefaultSize; target_line++)
                                        {
                                            for (var target_sample = 0; target_sample < TerrainPatch.DefaultSize; target_sample++)
                                            {
                                                kernel1(launchDimension, target_line, target_sample, line_offset, sample_offset, line_size, sample_size, d_min, d_max, d_step, gpu_caster_points, gpu_matrices, gpu_horizon, gpu_test_floats);
                                            }
                                        }
                                    }
                                    else
                                    {
                                        kernel1(launchDimension, 0, 0, line_offset, sample_offset, line_size, sample_size, d_min, d_max, d_step, gpu_caster_points, gpu_matrices, gpu_horizon, gpu_test_floats);
                                    }

                                    gpu_horizon.CopyTo(cpu_horizon, 0, 0, cpu_horizon_size);
                                    gpu_test_floats.CopyTo(test_floats, 0, 0, test_floats_size);

                                    // Update the horizons
                                    for (var line = 0; line < TerrainPatch.DefaultSize; line++)
                                    {
                                        for (var sample = 0; sample < TerrainPatch.DefaultSize; sample++)
                                        {
                                            var offset = (line * TerrainPatch.DefaultSize + sample) * Horizon.HorizonSamples;
                                            var buffer = target.Horizons[line][sample].Buffer;
                                            for (var i = 0; i < Horizon.HorizonSamples; i++)
                                            {
                                                buffer[i] = cpu_horizon[i + offset];
                                            }
                                        }
                                    }
                                }
                }
            }
        }
        /// <summary>
        /// Update the horizons of a patch based on a list of shadow casters.
        /// The horizons will be in slope, not angle, format
        /// </summary>
        /// <param name="target"></param>
        /// <param name="casters"></param>
        public void UpdateHorizons(TerrainPatch target, List <TerrainPatch> casters)
        {
            Debug.Assert(Terrain != null);
            if (casters.Count < 1)
            {
                return;
            }
            using (var context = new Context())
            {
                AcceleratorId aid = Accelerator.Accelerators.Where(id => id.AcceleratorType == AcceleratorType.Cuda).FirstOrDefault();
                if (aid.AcceleratorType != AcceleratorType.Cuda)
                {
                    Console.WriteLine(@"There is no CUDA accelerator present.  Doing nothing.");
                    return;
                }
                using (var accelerator = Accelerator.Create(context, aid))
                {
                    target.FillPoints(Terrain);
                    target.FillMatricesRelativeToPoint(Terrain, target.Points[0][0]);

                    // Matrices
                    var cpu_matrices_size = target.Height * target.Width * 12;
                    var basePoint         = target.Points[0][0];
                    var cpu_matrices      = MakeCPUMatrices(target);

                    // Horizon (load from target)
                    var cpu_horizon_size = target.Height * target.Width * Horizon.HorizonSamples;
                    var cpu_horizon      = new int[cpu_horizon_size];
                    for (var line = 0; line < TerrainPatch.DefaultSize; line++)
                    {
                        for (var sample = 0; sample < TerrainPatch.DefaultSize; sample++)
                        {
                            var offset = (line * TerrainPatch.DefaultSize + sample) * Horizon.HorizonSamples;
                            var buffer = target.Horizons[line][sample].Buffer;
                            for (var i = 0; i < Horizon.HorizonSamples; i++)
                            {
                                cpu_horizon[i + offset] = SlopeToEncoding(buffer[i]);
                            }
                        }
                    }

                    // Caster points
                    var cpu_caster_points_size = casters[0].Width * casters[0].Height * 3;
                    var cpu_caster_points      = new float[cpu_caster_points_size];

                    // test array
                    var cpu_test_array = new float[20];

                    using (var gpu_matrices = accelerator.Allocate <float>(cpu_matrices_size))
                        using (var gpu_horizon = accelerator.Allocate <int>(cpu_horizon_size))
                            using (var gpu_caster_points = accelerator.Allocate <float>(cpu_caster_points_size))
                                using (var gpu_test_array = accelerator.Allocate <float>(cpu_test_array.Length))
                                {
                                    gpu_matrices.CopyFrom(cpu_matrices, 0, 0, cpu_matrices_size);
                                    gpu_horizon.CopyFrom(cpu_horizon, 0, 0, cpu_horizon_size);

                                    var groupSize       = accelerator.MaxNumThreadsPerGroup;
                                    var launchDimension = new GroupedIndex2(
                                        new Index2(128, 128), // (data.Length + groupSize - 1) / groupSize,  // Compute the number of groups (round up)
                                        new Index2(1, 128));

                                    var kernel1 = accelerator.LoadSharedMemoryStreamKernel1 <GroupedIndex2, ArrayView <float>, ArrayView <float>, ArrayView <int>, ArrayView <float>, ArrayView <int> >(ShadowKernel1);

                                    //var stopwatch = new Stopwatch();
                                    //stopwatch.Start();

                                    foreach (var caster in casters)
                                    {
                                        caster.FillPoints(Terrain);
                                        CopyPointsToCpuArray(caster, basePoint, cpu_caster_points);
                                        gpu_caster_points.CopyFrom(cpu_caster_points, 0, 0, cpu_caster_points_size);

                                        kernel1(launchDimension, gpu_caster_points, gpu_matrices, gpu_horizon, gpu_test_array);

                                        accelerator.Synchronize();
                                    }

                                    // Copy out data
                                    gpu_horizon.CopyTo(cpu_horizon, 0, 0, cpu_horizon_size);
                                    gpu_test_array.CopyTo(cpu_test_array, 0, 0, cpu_test_array.Length);

                                    //stopwatch.Stop();
                                    //Console.WriteLine($"kernel time={stopwatch.Elapsed} cpu_horizon.Max()={cpu_horizon.Max()} cpu_horizon[0]={cpu_horizon[0]}");

                                    // Update the horizons
                                    for (var line = 0; line < TerrainPatch.DefaultSize; line++)
                                    {
                                        for (var sample = 0; sample < TerrainPatch.DefaultSize; sample++)
                                        {
                                            var offset = (line * TerrainPatch.DefaultSize + sample) * Horizon.HorizonSamples;
                                            var buffer = target.Horizons[line][sample].Buffer;
                                            for (var i = 0; i < Horizon.HorizonSamples; i++)
                                            {
                                                buffer[i] = EncodingToSlope(cpu_horizon[i + offset]);
                                            }
                                        }
                                    }

                                    //Console.WriteLine($"  max slope={cpu_horizon.Select(EncodingToSlope).Max()}");
                                }
                }
            }
        }