public unsafe void ReadTrack(int track, DiskReaderParams2 pars, ScanMode scanMode)
        {
            // Массив sectors:
            // D0 - Признак обработанного сектора при чтении трека в одной попытке. Обнуляется перед каждой попыткой чтения трека.
            // D1 - Используется в блоке чтения заголовков. Отмечаются сектора заголовки которых были найдены. Вне этого блока не используется.
            // D2 - Запрет чтения сектора из-за того что его заголовок был прочитан SectorReadAttempts раз и не был найден. Параметр сохраняется между попытками чтения трека.

            TrackFormat trackF = pars.Image.Tracks[track];

            const int sectorArrayLen = 50;
            int *     sectors        = stackalloc int[sectorArrayLen];

            for (int i = 0; i < sectorArrayLen; i++)
            {
                sectors[i] = 0;
            }
            bool trackScanned = false;

            for (int attempt = 0; attempt < pars.SectorReadAttempts; attempt++)
            {
                if (Aborted)
                {
                    return;
                }

                // Сканирование трека.

                if (
                    ((scanMode == ScanMode.Once && !trackScanned) ||
                     (scanMode == ScanMode.UnscannedOnly && trackF.FormatName == TrackFormatName.Unscanned) ||
                     scanMode == ScanMode.EachTrackRead)

                    && (trackF.MaxGap() > 128 + TrackFormat.MinSectorHeaderSize || trackF.ContainsCalculatedTime())
                    )
                {
                    // workTrackFormat и longestTrackFormat используются чтобы не плодить объекты.
                    // Объект scanFormatBuffer используется функциями ScanFormat и CombineFormats, поэтому его брать нельзя.

                    trackF.Scanning    = true;
                    trackF.MapModified = true;
                    if (ScanFormat(workTrackFormat, track, true))
                    {
                        for (int i = 0; i < workTrackFormat.Layout.Cnt; i++)
                        {
                            workTrackFormat.Layout.Data[i].MapCellValue = MapCell.Unprocessed;
                        }
                        if (CombineFormats(track, trackF, workTrackFormat, longestTrackFormat))
                        {
                            trackF.Assign(longestTrackFormat);
                            trackF.MapModified = true;
                        }
                    }
                    trackF.Scanning    = false;
                    trackF.MapModified = true;
                    trackScanned       = true;
                    if (Aborted)
                    {
                        return;
                    }
                }

                if (trackF.Layout.Cnt > sectorArrayLen)
                {
                    Log.Error?.Out($"Число секторов превышает размер рабочего массива: {trackF.Layout.Cnt}");
                    throw new Exception();
                }
                for (int i = 0; i < trackF.Layout.Cnt; i++)
                {
                    sectors[i] &= ~1;
                }
                bool wasError = false;
                trackTimer.Restart();
                int        skip             = 0;
                int        processedSectors = 0;
                int        sectorCounter    = 0;
                SectorInfo diskSector;
                while (processedSectors < trackF.Layout.Cnt)
                {
                    diskSector = trackF.Layout.Data[sectorCounter];
                    int sectorIndex = sectorCounter;
                    sectorCounter++;
                    skip++;
                    if ((sectors[sectorIndex] & 1) != 0)
                    {
                        continue;
                    }
                    sectors[sectorIndex] |= 1;
                    processedSectors++;
                    if ((sectors[sectorIndex] & 4) != 0)
                    {
                        continue;
                    }
                    if (trackF.Layout.Data[sectorIndex].ProcessResult == SectorProcessResult.Good)
                    {
                        continue;
                    }
                    skip = 0;
                    pars.Map.MarkSectorAsProcessing(track, sectorIndex);
                    WinApi.RtlZeroMemory(memoryHandle, (UIntPtr)diskSector.SizeBytes);
                    int    error            = Driver.ReadSectorF(DriverHandle, memoryHandle, diskSector.Cylinder, diskSector.SectorNumber, diskSector.SizeCode, track & 1, diskSector.Head, 0x0a, 0xff);
                    double curTimeSinceSync = trackF.Timer.ElapsedMs;
                    bool   badWritten       = diskSector.ProcessResult == SectorProcessResult.Bad;
                    if (error == 0)
                    {
                        pars.Image.WriteGoodSector(memoryHandle, trackF, sectorIndex);
                    }
                    else
                    {
                        wasError = true;
                        // Если надо проверить CRC-Error заголовка: (error == 27 && curTimeSinceSync > trackF.SpinTime)
                        bool noHeader = error == 21 || error == 1112 || error == 27;
                        if (noHeader)
                        {
                            if (!badWritten)
                            {
                                pars.Image.WriteNoHeader(memoryHandle, trackF, sectorIndex);
                            }
                            Make30HeadPositionings(error, track);
                        }
                        else if (error == 23)
                        {
                            pars.Image.WriteBadSector(memoryHandle, trackF, sectorIndex);
                            badWritten = true;
                        }
                        else
                        {
                            Log.Info?.Out($"Необработанная ошибка при чтении сектора: {error}");
                        }
                    }
                    if (Aborted)
                    {
                        return;
                    }
                }
                Log.Trace?.Out($"Время чтения трека: {GP.ToString(trackTimer.ElapsedMs, 2)}");
                trackTimer.Restart();
                if (!wasError)
                {
                    break;
                }
            }
        }
        /// <summary>
        /// Чтение вперед или назад. Возвращает количество успешно прочитанных секторов.
        /// </summary>
        /// <returns>Возвращает количество успешно прочитанных секторов.</returns>
        public int Read(ScanMode scanMode, bool forward)
        {
            int goodSectors = Params.Image.GoodSectors;

            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GCLatencyMode oldGCLatencyMode = GCSettings.LatencyMode;

            try
            {
                GCSettings.LatencyMode = GCLatencyMode.LowLatency;
                int firstTrack   = Params.FirstTrack;
                int prevCylinder = -1;
                int lastTrack    = Params.LastTrack;
                trackTimer.Restart();
                int track = forward ? firstTrack : lastTrack - 1;
                for (int p = firstTrack; p < lastTrack; p++, track = forward ? track + 1 : track - 1)
                {
                    if (Aborted)
                    {
                        goto abort;
                    }
                    if (Params.Side == DiskSide.Side0 && (track % 2 != 0))
                    {
                        continue;
                    }
                    if (Params.Side == DiskSide.Side1 && (track % 2 != 1))
                    {
                        continue;
                    }
                    TrackFormat trackF = Params.Image[track];
                    bool        scan   = ((scanMode == ScanMode.Once) ||
                                          (scanMode == ScanMode.UnscannedOnly && trackF.FormatName == TrackFormatName.Unscanned) ||
                                          scanMode == ScanMode.EachTrackRead) &&
                                         (trackF.MaxGap() > 128 + TrackFormat.MinSectorHeaderSize || trackF.ContainsCalculatedTime());
                    if (!scan && trackF.NotGoodSectors == 0)
                    {
                        continue;
                    }
                    int cylinder = track / 2;
                    if (cylinder != prevCylinder)
                    {
                        Driver.Seek(DriverHandle, track);
                        if (Aborted)
                        {
                            goto abort;
                        }
                        prevCylinder = cylinder;
                    }
                    ReadTrack(track, Params, scanMode);
                }
                return(Params.Image.GoodSectors - goodSectors);

abort:
                Log.Info?.Out("Чтение прервано.");
            }
            finally
            {
                GCSettings.LatencyMode = oldGCLatencyMode;
                Params.Map?.ClearHighlight(MapCell.Processing);
            }
            return(Params.Image.GoodSectors - goodSectors);
        }